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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/.yardopts +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +208 -0
- data/LICENSE +165 -0
- data/README.md +33 -0
- data/Rakefile +34 -0
- data/app/controllers/compony_controller.rb +31 -0
- data/compony.gemspec +32 -0
- data/config/locales/de.yml +29 -0
- data/config/locales/en.yml +29 -0
- data/config/routes.rb +18 -0
- data/doc/resourceful_lifecycle.graphml +819 -0
- data/doc/resourceful_lifecycle.pdf +1564 -0
- data/lib/compony/component.rb +225 -0
- data/lib/compony/component_mixins/default/labelling.rb +77 -0
- data/lib/compony/component_mixins/default/standalone/resourceful_verb_dsl.rb +55 -0
- data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +56 -0
- data/lib/compony/component_mixins/default/standalone/verb_dsl.rb +47 -0
- data/lib/compony/component_mixins/default/standalone.rb +117 -0
- data/lib/compony/component_mixins/resourceful.rb +92 -0
- data/lib/compony/components/button.rb +59 -0
- data/lib/compony/components/form.rb +138 -0
- data/lib/compony/components/resourceful/destroy.rb +77 -0
- data/lib/compony/components/resourceful/edit.rb +96 -0
- data/lib/compony/components/resourceful/new.rb +95 -0
- data/lib/compony/components/with_form.rb +37 -0
- data/lib/compony/controller_mixin.rb +12 -0
- data/lib/compony/engine.rb +19 -0
- data/lib/compony/method_accessible_hash.rb +43 -0
- data/lib/compony/model_fields/anchormodel.rb +28 -0
- data/lib/compony/model_fields/association.rb +53 -0
- data/lib/compony/model_fields/base.rb +63 -0
- data/lib/compony/model_fields/boolean.rb +9 -0
- data/lib/compony/model_fields/currency.rb +9 -0
- data/lib/compony/model_fields/date.rb +9 -0
- data/lib/compony/model_fields/datetime.rb +9 -0
- data/lib/compony/model_fields/decimal.rb +6 -0
- data/lib/compony/model_fields/float.rb +6 -0
- data/lib/compony/model_fields/integer.rb +6 -0
- data/lib/compony/model_fields/phone.rb +15 -0
- data/lib/compony/model_fields/rich_text.rb +9 -0
- data/lib/compony/model_fields/string.rb +6 -0
- data/lib/compony/model_fields/text.rb +6 -0
- data/lib/compony/model_fields/time.rb +6 -0
- data/lib/compony/model_mixin.rb +88 -0
- data/lib/compony/request_context.rb +45 -0
- data/lib/compony/version.rb +11 -0
- data/lib/compony/view_helpers.rb +36 -0
- data/lib/compony.rb +268 -0
- data/lib/generators/component/USAGE +8 -0
- data/lib/generators/component/component_generator.rb +14 -0
- data/lib/generators/component/templates/component.rb.erb +4 -0
- 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,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,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,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,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
|