compony 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -0
  4. data/.yardopts +2 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +208 -0
  7. data/LICENSE +165 -0
  8. data/README.md +33 -0
  9. data/Rakefile +34 -0
  10. data/app/controllers/compony_controller.rb +31 -0
  11. data/compony.gemspec +32 -0
  12. data/config/locales/de.yml +29 -0
  13. data/config/locales/en.yml +29 -0
  14. data/config/routes.rb +18 -0
  15. data/doc/resourceful_lifecycle.graphml +819 -0
  16. data/doc/resourceful_lifecycle.pdf +1564 -0
  17. data/lib/compony/component.rb +225 -0
  18. data/lib/compony/component_mixins/default/labelling.rb +77 -0
  19. data/lib/compony/component_mixins/default/standalone/resourceful_verb_dsl.rb +55 -0
  20. data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +56 -0
  21. data/lib/compony/component_mixins/default/standalone/verb_dsl.rb +47 -0
  22. data/lib/compony/component_mixins/default/standalone.rb +117 -0
  23. data/lib/compony/component_mixins/resourceful.rb +92 -0
  24. data/lib/compony/components/button.rb +59 -0
  25. data/lib/compony/components/form.rb +138 -0
  26. data/lib/compony/components/resourceful/destroy.rb +77 -0
  27. data/lib/compony/components/resourceful/edit.rb +96 -0
  28. data/lib/compony/components/resourceful/new.rb +95 -0
  29. data/lib/compony/components/with_form.rb +37 -0
  30. data/lib/compony/controller_mixin.rb +12 -0
  31. data/lib/compony/engine.rb +19 -0
  32. data/lib/compony/method_accessible_hash.rb +43 -0
  33. data/lib/compony/model_fields/anchormodel.rb +28 -0
  34. data/lib/compony/model_fields/association.rb +53 -0
  35. data/lib/compony/model_fields/base.rb +63 -0
  36. data/lib/compony/model_fields/boolean.rb +9 -0
  37. data/lib/compony/model_fields/currency.rb +9 -0
  38. data/lib/compony/model_fields/date.rb +9 -0
  39. data/lib/compony/model_fields/datetime.rb +9 -0
  40. data/lib/compony/model_fields/decimal.rb +6 -0
  41. data/lib/compony/model_fields/float.rb +6 -0
  42. data/lib/compony/model_fields/integer.rb +6 -0
  43. data/lib/compony/model_fields/phone.rb +15 -0
  44. data/lib/compony/model_fields/rich_text.rb +9 -0
  45. data/lib/compony/model_fields/string.rb +6 -0
  46. data/lib/compony/model_fields/text.rb +6 -0
  47. data/lib/compony/model_fields/time.rb +6 -0
  48. data/lib/compony/model_mixin.rb +88 -0
  49. data/lib/compony/request_context.rb +45 -0
  50. data/lib/compony/version.rb +11 -0
  51. data/lib/compony/view_helpers.rb +36 -0
  52. data/lib/compony.rb +268 -0
  53. data/lib/generators/component/USAGE +8 -0
  54. data/lib/generators/component/component_generator.rb +14 -0
  55. data/lib/generators/component/templates/component.rb.erb +4 -0
  56. metadata +236 -0
@@ -0,0 +1,63 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Base
4
+ attr_reader :name
5
+ attr_reader :model_class
6
+ attr_reader :schema_key
7
+ attr_reader :extra_attrs
8
+
9
+ def multi?
10
+ !!@multi
11
+ end
12
+
13
+ def association?
14
+ !!@association
15
+ end
16
+
17
+ def initialize(name, model_class, **extra_attrs)
18
+ @name = name.to_sym
19
+ @model_class = model_class
20
+ @schema_key = name
21
+ @extra_attrs = extra_attrs
22
+ end
23
+
24
+ # Use this to display the label for this field, e.g. for columns, forms etc.
25
+ def label
26
+ @model_class.human_attribute_name(@name)
27
+ end
28
+
29
+ # Use this to display the value for this field applied to data
30
+ def value_for(data, controller: nil, **_)
31
+ # Default behavior
32
+ return transform_and_join(data.send(@name), controller:)
33
+ end
34
+
35
+ # Used for auto-providing Schemacop schemas.
36
+ # Returns a proc that is meant for instance_exec within a Schemacop3 hash block
37
+ def schema_line
38
+ # Default behavior
39
+ local_schema_key = @schema_key # Capture schema_key as it will not be available within the lambda
40
+ return proc { obj? local_schema_key }
41
+ end
42
+
43
+ # Used in form helper.
44
+ # Given a simpleform instance, returns the corresponding input to be supplied to the view.
45
+ def simpleform_input(form, _component, **input_opts)
46
+ return form.input @name, **input_opts
47
+ end
48
+
49
+ protected
50
+
51
+ # If given a scalar, calls the block on the scalar. If given a list, calls the block on every member and joins the result with ",".
52
+ def transform_and_join(data, controller:, &transform_block)
53
+ if data.is_a?(Enumerable)
54
+ data = data.compact.map(&transform_block) if block_given?
55
+ return controller.helpers.safe_join(data.compact, ', ')
56
+ else
57
+ data = transform_block.call(data) if block_given?
58
+ return data
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,9 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Boolean < Base
4
+ def value_for(data, controller: nil, **_)
5
+ return transform_and_join(data.send(@name), controller:) { |el| I18n.t("compony.boolean.#{el}") }
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Currency < Base
4
+ def value_for(data, controller: nil, **_)
5
+ return transform_and_join(data.send(@name), controller:) { |el| controller.helpers.number_to_currency(el) }
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Date < Base
4
+ def value_for(data, controller: nil, **_)
5
+ return transform_and_join(data.send(@name), controller:) { |el| el.nil? ? nil : I18n.l(el) }
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Datetime < Base
4
+ def value_for(data, controller: nil, **_)
5
+ return transform_and_join(data.send(@name), controller:) { |el| el.nil? ? nil : I18n.l(el) }
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Decimal < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Float < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Integer < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ module Compony
2
+ module ModelFields
3
+ # Requires 'phonelib' gem
4
+ class Phone < Base
5
+ def initialize(...)
6
+ fail('Please include gem "phonelib" to use the :phone field type.') unless defined?(Phonelib)
7
+ super
8
+ end
9
+
10
+ def value_for(data, controller: nil, **_)
11
+ return transform_and_join(data.send(@name), controller:) { |el| Phonelib.parse(el).international }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module Compony
2
+ module ModelFields
3
+ class RichText < Base
4
+ def simpleform_input(form, _component, **input_opts)
5
+ return form.input @name, **input_opts.merge(as: :rich_text_area)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ module Compony
2
+ module ModelFields
3
+ class String < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Text < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Time < Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,88 @@
1
+ module Compony
2
+ module ModelMixin
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :fields, default: {}
7
+ class_attribute :feasibility_preventions, default: {}
8
+ class_attribute :primary_key_type_key, default: :integer
9
+
10
+ class_attribute :autodetect_feasibilities_completed, default: false
11
+ end
12
+
13
+ class_methods do
14
+ # DSL method, defines a new field which will be translated and can be added to field groups
15
+ # For virtual attributes, you must pass a type explicitely, otherwise it's auto-infered.
16
+ def field(name, type, **extra_attrs)
17
+ name = name.to_sym
18
+ self.fields = fields.dup
19
+ fields[name] = Compony.model_field_class_for(type.to_s.camelize).new(name, self, **extra_attrs)
20
+ end
21
+
22
+ # DSL method, sets the primary key type
23
+ def primary_key_type(new_type)
24
+ unless %i[integer string].include?(new_type.to_sym)
25
+ fail("#{self} is declaring primary_key_type as #{new_type.inspect} but only :integer and :string are supported at this time.")
26
+ end
27
+ self.primary_key_type_key = new_type.to_sym
28
+ end
29
+
30
+ # DSL method, part of the Feasibility feature
31
+ # Block must return `false` if the action should be prevented.
32
+ def prevent(action_name, message, &block)
33
+ self.feasibility_preventions = feasibility_preventions.dup # Prevent cross-class contamination
34
+ feasibility_preventions[action_name.to_sym] ||= []
35
+ feasibility_preventions[action_name.to_sym] << MethodAccessibleHash.new(action_name:, message:, block:)
36
+ end
37
+
38
+ # DSL method, part of the Feasibility feature
39
+ # Skips autodetection of feasibilities
40
+ def skip_autodetect_feasibilities
41
+ self.autodetect_feasibilities_completed = true
42
+ end
43
+
44
+ def autodetect_feasibilities!
45
+ return if autodetect_feasibilities_completed
46
+ # Add a prevention that reflects the `has_many` `dependent' properties. Avoids that users can press buttons that will result in a failed destroy.
47
+ reflect_on_all_associations.select { |assoc| %i[restrict_with_exception restrict_with_error].include? assoc.options[:dependent] }.each do |assoc|
48
+ prevent(:destroy, I18n.t('compony.feasibility.has_dependent_models', dependent_class: I18n.t(assoc.klass.model_name.plural.humanize))) do
49
+ public_send(assoc.name).any?
50
+ end
51
+ end
52
+ self.autodetect_feasibilities_completed = true
53
+ end
54
+ end
55
+
56
+ # Retrieves feasibility for the given instance
57
+ # Calling this with an invalid action name will always return true.
58
+ def feasible?(action_name, recompute: false)
59
+ action_name = action_name.to_sym
60
+ @feasibility_messages ||= {}
61
+ # Abort if check has already run and recompute is false
62
+ if @feasibility_messages[action_name].nil? || recompute
63
+ # Lazily autodetect feasibilities
64
+ self.class.autodetect_feasibilities!
65
+ # Compute feasibility and gather messages
66
+ @feasibility_messages[action_name] = []
67
+ feasibility_preventions[action_name]&.each do |prevention|
68
+ if instance_exec(&prevention.block)
69
+ @feasibility_messages[action_name] << prevention.message
70
+ end
71
+ end
72
+ end
73
+ return @feasibility_messages[action_name].none?
74
+ end
75
+
76
+ def feasibility_messages(action_name)
77
+ action_name = action_name.to_sym
78
+ feasible?(action_name) if @feasibility_messages&.[](action_name).nil? # If feasibility check hasn't been performed yet for this action, perform it now
79
+ return @feasibility_messages[action_name]
80
+ end
81
+
82
+ def full_feasibility_messages(action_name)
83
+ text = feasibility_messages(action_name).join(', ').upcase_first
84
+ text += '.' if text.present?
85
+ return text
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,45 @@
1
+ module Compony
2
+ # @api description
3
+ # This encapsulates useful methods for accessing data within a request.
4
+ class RequestContext < Dslblend::Base
5
+ # Allow explicit access to the controller object. All controller methods are delgated.
6
+ attr_reader :controller
7
+ attr_reader :helpers
8
+ attr_reader :local_assigns
9
+
10
+ def initialize(component, controller, *additional_providers, helpers: nil, locals: {})
11
+ # DSL provider is this class, controller is an additional provider, main provider should be the component
12
+ # Note: we have to manually set the main provider here as the auto-detection sets it to the VerbDsl instance around the block,
13
+ # leading to undesired caching effects (e.g. components being re-used, even if the comp_opts have changed)
14
+ @controller = controller
15
+ @helpers = helpers || controller.helpers
16
+ @local_assigns = locals.with_indifferent_access
17
+ super(@helpers, @controller, *additional_providers, main_provider: component)
18
+ end
19
+
20
+ def evaluate_with_backfire(&)
21
+ evaluate(backfire_vars: true, &)
22
+ end
23
+
24
+ def component
25
+ @_main_provider
26
+ end
27
+
28
+ # Explicit accessor to this object. As Dslblend hides where a method comes from, this makes code modifying the request context more explicit.
29
+ # This is for instance useful when a component wishes to extend the request context with a module in order to define methods directly on the context.
30
+ def request_context
31
+ self
32
+ end
33
+
34
+ # Provide access to local assigns as if it were a Rails context
35
+ def method_missing(method, *args, **kwargs, &)
36
+ return @local_assigns[method] if @local_assigns.key?(method)
37
+ return super
38
+ end
39
+
40
+ def respond_to_missing?(method, include_all)
41
+ return true if @local_assigns.key?(method)
42
+ return super
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ module Compony
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ PATCH = 1
6
+
7
+ EDGE = false
8
+
9
+ LABEL = [MAJOR, MINOR, PATCH, EDGE ? 'edge' : nil].compact.join('.')
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ module Compony
2
+ # @api description
3
+ # Methods in this module are available in content blocks and Rails views.
4
+ # Rule of thumb: this holds methods that require a view context and results are rendered immediately.
5
+ # @see Compony Compony for standalone/pure helpers
6
+ module ViewHelpers
7
+ # Use this in your application layout to render all actions of the current root component.
8
+ def compony_actions
9
+ return nil unless Compony.root_comp
10
+ Compony.root_comp.render_actions(self, wrapper_class: 'root-actions', action_class: 'root-action')
11
+ end
12
+
13
+ # Renders a link to a component given a comp and model or family. If authentication is configured
14
+ # and the current user has insufficient permissions to access the target object, the link is not displayed.
15
+ # @param comp_name_or_cst [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all`
16
+ # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component,
17
+ # or an instance implementing `model_name` from which the family name is auto-generated. Examples:
18
+ # `Users`, `'Users'`, `:users`, `User.first`
19
+ # @param link_args [Array] Positional arguments that will be passed to the Rails `link_to` helper
20
+ # @param label_opts [Hash] Options hash that will be passed to the label method (see {Compony::ComponentMixins::Default::Labelling#label})
21
+ # @param link_kwargs [Hash] Named arguments that will be passed to the Rails `link_to` helper
22
+ def compony_link(comp_name_or_cst, model_or_family_name_or_cst, *link_args, label_opts: {}, **link_kwargs)
23
+ model = model_or_family_name_or_cst.respond_to?(:model_name) ? model_or_family_name_or_cst : nil
24
+ comp = Compony.comp_class_for!(comp_name_or_cst, model_or_family_name_or_cst).new(data: model)
25
+ return unless comp.standalone_access_permitted_for?(self)
26
+ return helpers.link_to(comp.label(model, **label_opts), Compony.path(comp.comp_name, comp.family_name, model), *link_args, **link_kwargs)
27
+ end
28
+
29
+ # Given a component and a family/model, this instanciates and renders a button component.
30
+ # @see Compony#button Check Compony.button for accepted params
31
+ # @see Compony::Components::Button Compony::Components::Button: the default underlying implementation
32
+ def compony_button(...)
33
+ Compony.button(...).render(helpers.controller)
34
+ end
35
+ end
36
+ end
data/lib/compony.rb ADDED
@@ -0,0 +1,268 @@
1
+ # @api description
2
+ # Root module, containing confguration and pure helpers. For
3
+ # the setters, create an initializer `config/initializers/compony.rb` and call
4
+ # them from there.
5
+ # @see Compony::ViewHelpers Compony::ViewHelpers for helpers that require a view context and render results immediately
6
+ module Compony
7
+ ##########=====-------
8
+ # Configuration writers
9
+ ##########=====-------
10
+
11
+ # Setter for the global button component class. This allows you to implement a
12
+ # custom button component and have all Compony button helpers use your custom
13
+ # button component instead of {Compony::Components::Button}.
14
+ # @param button_component_class [Class] Your custom button component class (inherit from {Compony::Components::Button} or {Compony::Component})
15
+ def self.button_component_class=(button_component_class)
16
+ @button_component_class = button_component_class
17
+ end
18
+
19
+ # Setter for the global field namespaces. This allows you to implement custom
20
+ # Fields, be it new ones or overrides for existing Compony model fields.
21
+ # Must give an array of strings of namespaces that contain field classes named after
22
+ # the field type. The array is queried in order, if the first namespace does not
23
+ # contain the class we're looking for, the next is considered and so on.
24
+ # The classes defined in the namespace must inherit from Compony::ModelFields::Base
25
+ def self.model_field_namespaces=(model_field_namespaces)
26
+ @model_field_namespaces = model_field_namespaces
27
+ end
28
+
29
+ # Setter for the name of the Rails `before_action` that should be called to
30
+ # ensure that users are authenticated before accessing the component. For
31
+ # instance, implement a method `def enforce_authentication` in your
32
+ # `ApplicationController`. In the method, make sure the user has a session and
33
+ # redirect to the login page if they don't. <br> The action must be accessible
34
+ # by {ComponyController} and the easiest way to achieve this is to implement
35
+ # the action in your `ApplicationController`. If this is never called,
36
+ # authentication is disabled.
37
+ def self.authentication_before_action=(authentication_before_action)
38
+ @authentication_before_action = authentication_before_action.to_sym
39
+ end
40
+
41
+ ##########=====-------
42
+ # Configuration readers
43
+ ##########=====-------
44
+
45
+ # Getter for the global button component class.
46
+ # @see Compony#button_component_class= Explanation of button_component_class (documented in the corresponding setter)
47
+ def self.button_component_class
48
+ @button_component_class ||= Components::Button
49
+ @button_component_class = const_get(@button_component_class) if @button_component_class.is_a?(String)
50
+ return @button_component_class
51
+ end
52
+
53
+ # Getter for the global field namespaces.
54
+ # @see Compony#model_field_namespaces= Explanation of model_field_namespaces (documented in the corresponding setter)
55
+ def self.model_field_namespaces
56
+ return @model_field_namespaces ||= ['Compony::ModelFields']
57
+ end
58
+
59
+ # Getter for the name of the Rails `before_action` that enforces authentication.
60
+ # @see Compony#authentication_before_action= Explanation of authentication_before_action (documented in the corresponding setter)
61
+ def self.authentication_before_action
62
+ @authentication_before_action
63
+ end
64
+
65
+ ##########=====-------
66
+ # Application-wide available pure helpers
67
+ ##########=====-------
68
+
69
+ # Generates a Rails path to a component. Examples: `Compony.path(:index, :users)`, `Compony.path(:show, User.first)`
70
+ # @param comp_name_or_cst [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all`
71
+ # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component,
72
+ # or an instance implementing `model_name` from which the family name is auto-generated. Examples:
73
+ # `Users`, `'Users'`, `:users`, `User.first`
74
+ # @param args_for_path_helper [Array] Positional arguments passed to the Rails helper
75
+ # @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`,
76
+ # the param `id` defaults to the passed model's ID.
77
+ def self.path(comp_name_or_cst, model_or_family_name_or_cst, *args_for_path_helper, **kwargs_for_path_helper)
78
+ # Extract model if any, to get the ID
79
+ kwargs_for_path_helper.merge!(id: model_or_family_name_or_cst.id) if model_or_family_name_or_cst.respond_to?(:model_name)
80
+ return Rails.application.routes.url_helpers.send(
81
+ "#{path_helper_name(comp_name_or_cst, model_or_family_name_or_cst)}_path",
82
+ *args_for_path_helper,
83
+ **kwargs_for_path_helper
84
+ )
85
+ end
86
+
87
+ # Given a component and a family/model, this returns the matching component class if any, or nil if the component does not exist.
88
+ # @param comp_name_or_cst [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all`
89
+ # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component,
90
+ # or an instance implementing `model_name` from which the family name is auto-generated. Examples:
91
+ # `Users`, `'Users'`, `:users`, `User.first`
92
+ def self.comp_class_for(comp_name_or_cst, model_or_family_name_or_cst)
93
+ family_cst_str = family_name_for(model_or_family_name_or_cst).camelize
94
+ comp_cst_str = comp_name_or_cst.to_s.camelize
95
+ return nil unless ::Components.const_defined?(family_cst_str)
96
+ family_constant = ::Components.const_get(family_cst_str)
97
+ return nil unless family_constant.const_defined?(comp_cst_str)
98
+ return family_constant.const_get(comp_cst_str)
99
+ end
100
+
101
+ # As above but fails if none found
102
+ def self.comp_class_for!(comp_name_or_cst, model_or_family_name_or_cst)
103
+ comp_class_for(comp_name_or_cst, model_or_family_name_or_cst) || fail(
104
+ "No component found for [#{comp_name_or_cst.inspect}, #{model_or_family_name_or_cst.inspect}]"
105
+ )
106
+ end
107
+
108
+ # Given a component and a family, this returns the name of the Rails URL helper returning the path to this component.<br>
109
+ # The parameters are the same as for {Compony#rails_action_name}.<br>
110
+ # Example usage: `send("#{path_helper_name(:index, :users)}_url)`
111
+ # @see Compony#path
112
+ # @see Compony#rails_action_name rails_action_name for the accepted params
113
+ def self.path_helper_name(...)
114
+ "#{rails_action_name(...)}_comp"
115
+ end
116
+
117
+ # Given a component and a family, this returns the name of the ComponyController action for this component.<br>
118
+ # Optionally can pass a name for extra standalone configs.
119
+ # @param comp_name_or_cst [String,Symbol] Name of the component the action points to.
120
+ # @param model_or_family_name_or_cst [String,Symbol] Name of the family the action points to.
121
+ # @param name [String,Symbol] If referring to an extra standalone entrypoint, specify its name using this param.
122
+ # @see Compony#path
123
+ def self.rails_action_name(comp_name_or_cst, model_or_family_name_or_cst, name = nil)
124
+ [name.presence, comp_name_or_cst.to_s.underscore, family_name_for(model_or_family_name_or_cst)].compact.join('_')
125
+ end
126
+
127
+ # Given a component and a family/model, this instanciates and returns a button component.
128
+ # @param comp_name_or_cst [String,Symbol] The component that should be loaded, for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all`
129
+ # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component,
130
+ # or an instance implementing `model_name` from which the family name is auto-generated. Examples:
131
+ # `Users`, `'Users'`, `:users`, `User.first`
132
+ # @param label_opts [Hash] Options hash that will be passed to the label method (see {Compony::ComponentMixins::Default::Labelling#label})
133
+ # @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
134
+ # @param override_kwargs [Hash] Override button options, see options for {Compony::Components::Button}
135
+ # @see Compony::ViewHelpers#compony_button View helper providing a wrapper for this method that immediately renders a button.
136
+ # @see Compony::Components::Button Compony::Components::Button: the default underlying implementation
137
+ # @todo add doc for feasibility
138
+ def self.button(comp_name_or_cst,
139
+ model_or_family_name_or_cst,
140
+ label_opts: nil,
141
+ params: nil,
142
+ feasibility_action: nil,
143
+ feasibility_target: nil,
144
+ **override_kwargs)
145
+ label_opts ||= button_defaults[:label_opts] || {}
146
+ params ||= button_defaults[:params] || {}
147
+ model = model_or_family_name_or_cst.respond_to?(:model_name) ? model_or_family_name_or_cst : nil
148
+ target_comp_instance = Compony.comp_class_for!(comp_name_or_cst, model_or_family_name_or_cst).new(data: model)
149
+ feasibility_action ||= button_defaults[:feasibility_action] || comp_name_or_cst.to_s.underscore.to_sym
150
+ feasibility_target ||= button_defaults[:feasibility_target] || model
151
+ options = {
152
+ label: target_comp_instance.label(model, **label_opts),
153
+ icon: target_comp_instance.icon,
154
+ color: target_comp_instance.color,
155
+ path: Compony.path(target_comp_instance.comp_name, target_comp_instance.family_name, model, **params),
156
+ visible: ->(controller) { target_comp_instance.standalone_access_permitted_for?(controller) }
157
+ }
158
+ if feasibility_target
159
+ options.merge!({
160
+ enabled: feasibility_target.feasible?(feasibility_action),
161
+ title: feasibility_target.full_feasibility_messages(feasibility_action).presence
162
+ })
163
+ end
164
+ options.merge!(override_kwargs.symbolize_keys)
165
+ return Compony.button_component_class.new(**options.symbolize_keys)
166
+ end
167
+
168
+ # Returns the current root component, if any
169
+ def self.root_comp
170
+ RequestStore.store[:compony_root_comp]
171
+ end
172
+
173
+ # Given a family name or a model-like class, this returns the suitable family name as String.
174
+ # @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component,
175
+ # or an instance implementing `model_name` from which the family name is auto-generated. Examples:
176
+ # `Users`, `'Users'`, `:users`, `User.first`
177
+ def self.family_name_for(model_or_family_name_or_cst)
178
+ if model_or_family_name_or_cst.respond_to?(:model_name)
179
+ return model_or_family_name_or_cst.model_name.plural
180
+ else
181
+ return model_or_family_name_or_cst.to_s.underscore
182
+ end
183
+ end
184
+
185
+ # Getter for current button defaults
186
+ # @todo document params
187
+ def self.button_defaults
188
+ RequestStore.store[:button_defaults] || {}
189
+ end
190
+
191
+ # Overwrites the keys of the current button defaults by the ones provided during the execution of a given block and restores them afterwords.
192
+ # @todo document params
193
+ def self.with_button_defaults(**keys_to_overwrite, &block)
194
+ # Lazy initialize butto_defaults store if it hasn't been yet
195
+ RequestStore.store[:button_defaults] ||= {}
196
+ keys_to_overwrite.transform_keys!(&:to_sym)
197
+ old_values = {}
198
+ newly_defined_keys = keys_to_overwrite.keys - RequestStore.store[:button_defaults].keys
199
+ keys_to_overwrite.each do |key, new_value|
200
+ # Assign new value
201
+ old_values[key] = RequestStore.store[:button_defaults][key]
202
+ RequestStore.store[:button_defaults][key] = new_value
203
+ end
204
+ return_value = block.call
205
+ # Restore previous value
206
+ keys_to_overwrite.each do |key, _new_value|
207
+ RequestStore.store[:button_defaults][key] = old_values[key]
208
+ end
209
+ # Undefine keys that were not there previously
210
+ newly_defined_keys.each { |key| RequestStore.store[:button_defaults].delete(key) }
211
+ return return_value
212
+ end
213
+
214
+ # Goes through model_field_namespaces and returns the first hit for the given constant
215
+ # @param constant [Constant] The constant that is searched, e.g. RichText -> would return e.g. Compony::ModelFields::RichText
216
+ def self.model_field_class_for(constant)
217
+ @model_field_namespaces.each do |model_field_namespace|
218
+ model_field_namespace = model_field_namespace.constantize if model_field_namespace.is_a?(::String)
219
+ if model_field_namespace.const_defined?(constant, false)
220
+ return model_field_namespace.const_get(constant, false)
221
+ end
222
+ end
223
+ fail("No `model_field_namespace` implements ...::#{constant}. Configured namespaces: #{Compony.model_field_namespaces.inspect}")
224
+ end
225
+ end
226
+
227
+ require 'cancancan'
228
+ require 'dslblend'
229
+ require 'dyny'
230
+ require 'request_store'
231
+ require 'schemacop'
232
+ require 'simple_form'
233
+
234
+ require 'compony/engine'
235
+ require 'compony/model_fields/base'
236
+ require 'compony/model_fields/association'
237
+ require 'compony/model_fields/anchormodel'
238
+ require 'compony/model_fields/boolean'
239
+ require 'compony/model_fields/currency'
240
+ require 'compony/model_fields/date'
241
+ require 'compony/model_fields/datetime'
242
+ require 'compony/model_fields/decimal'
243
+ require 'compony/model_fields/float'
244
+ require 'compony/model_fields/integer'
245
+ require 'compony/model_fields/phone'
246
+ require 'compony/model_fields/rich_text'
247
+ require 'compony/model_fields/string'
248
+ require 'compony/model_fields/text'
249
+ require 'compony/model_fields/time'
250
+ require 'compony/component_mixins/default/standalone'
251
+ require 'compony/component_mixins/default/standalone/standalone_dsl'
252
+ require 'compony/component_mixins/default/standalone/verb_dsl'
253
+ require 'compony/component_mixins/default/standalone/resourceful_verb_dsl'
254
+ require 'compony/component_mixins/default/labelling'
255
+ require 'compony/component_mixins/resourceful'
256
+ require 'compony/component'
257
+ require 'compony/components/button'
258
+ require 'compony/components/form'
259
+ require 'compony/components/with_form'
260
+ require 'compony/components/resourceful/new'
261
+ require 'compony/components/resourceful/edit'
262
+ require 'compony/components/resourceful/destroy'
263
+ require 'compony/method_accessible_hash'
264
+ require 'compony/model_mixin'
265
+ require 'compony/request_context'
266
+ require 'compony/version'
267
+ require 'compony/view_helpers'
268
+ require 'compony/controller_mixin'
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generate a single Compony component
3
+
4
+ Example:
5
+ bin/rails generate component Users::New
6
+
7
+ This will create:
8
+ app/components/users/new.rb
@@ -0,0 +1,14 @@
1
+ class ComponentGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+ def add_component
5
+ segments = name.underscore.split('/')
6
+ fail('NAME must be of the form Family::ComponentName or family/component_name') if segments.size != 2
7
+ @family, @comp = segments
8
+ @family = @family.pluralize # Force plural
9
+ @family_cst = @family.camelize.pluralize # Force plural
10
+ @comp_cst = @comp.camelize # Tolerate singular and plural
11
+
12
+ template 'component.rb.erb', "app/components/#{@family}/#{@comp}.rb"
13
+ end
14
+ end
@@ -0,0 +1,4 @@
1
+ class Components::<%= @family_cst %>::<%= @comp_cst %> < Compony::Component
2
+ setup do
3
+ end
4
+ end