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,225 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
class Component
|
|
3
|
+
# Include all functionality that was moved to default mixins for better overview
|
|
4
|
+
Compony::ComponentMixins::Default.constants.each { |cst| include Compony::ComponentMixins::Default.const_get(cst) }
|
|
5
|
+
|
|
6
|
+
class_attribute :setup_blocks
|
|
7
|
+
|
|
8
|
+
attr_reader :parent_comp
|
|
9
|
+
attr_reader :comp_opts
|
|
10
|
+
|
|
11
|
+
# root comp: component that is registered to be root of the application.
|
|
12
|
+
# parent comp: component that is registered to be the parent of this comp. If there is none, this is the root comp.
|
|
13
|
+
|
|
14
|
+
# DSL method
|
|
15
|
+
def self.setup(&block)
|
|
16
|
+
fail("`setup` expects a block in #{inspect}.") unless block_given?
|
|
17
|
+
self.setup_blocks ||= []
|
|
18
|
+
self.setup_blocks = setup_blocks.dup # This is required to prevent the parent class to see children's setup blocks.
|
|
19
|
+
setup_blocks << block
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(parent_comp = nil, index: 0, **comp_opts)
|
|
23
|
+
@parent_comp = parent_comp
|
|
24
|
+
@sub_comps = []
|
|
25
|
+
@index = index
|
|
26
|
+
@comp_opts = comp_opts
|
|
27
|
+
@before_render_block = nil
|
|
28
|
+
@content_blocks = []
|
|
29
|
+
@actions = []
|
|
30
|
+
@skipped_actions = Set.new
|
|
31
|
+
|
|
32
|
+
init_standalone
|
|
33
|
+
init_labelling
|
|
34
|
+
|
|
35
|
+
fail "#{inspect} is missing a call to `setup`." unless setup_blocks&.any?
|
|
36
|
+
|
|
37
|
+
setup_blocks.each do |setup_block|
|
|
38
|
+
instance_exec(&setup_block)
|
|
39
|
+
end
|
|
40
|
+
check_config!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def inspect
|
|
44
|
+
"#<#{self.class.name}:#{hash}>"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the current root comp.
|
|
48
|
+
# Do not overwrite.
|
|
49
|
+
def root_comp
|
|
50
|
+
return self unless parent_comp
|
|
51
|
+
return parent_comp.root_comp
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns whether or not this is the root comp.
|
|
55
|
+
# Do not overwrite.
|
|
56
|
+
def root_comp?
|
|
57
|
+
parent_comp.nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns an identifier describing this component. Must be unique among simplings under the same parent_comp.
|
|
61
|
+
# Do not override.
|
|
62
|
+
def id
|
|
63
|
+
"#{family_name}_#{comp_name}_#{@index}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns the id path from the root_comp.
|
|
67
|
+
# Do not overwrite.
|
|
68
|
+
def path
|
|
69
|
+
if root_comp?
|
|
70
|
+
id
|
|
71
|
+
else
|
|
72
|
+
"#{parent_comp.path}/#{id}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns a hash for the path. Used for params prefixing.
|
|
77
|
+
# Do not overwrite.
|
|
78
|
+
def path_hash
|
|
79
|
+
Digest::SHA1.hexdigest(path)[..4]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Given an unprefixed name of a param, adds the path hash
|
|
83
|
+
# Do not overwrite.
|
|
84
|
+
def param_name(unprefixed_param_name)
|
|
85
|
+
"#{path_hash}_#{unprefixed_param_name}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Instanciate a component with `self` as a parent
|
|
89
|
+
def sub_comp(component_class, **comp_opts)
|
|
90
|
+
sub = component_class.new(self, index: @sub_comps.count, **comp_opts)
|
|
91
|
+
@sub_comps << sub
|
|
92
|
+
return sub
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns the name of the module constant (=family) of this component. Do not override.
|
|
96
|
+
def family_cst
|
|
97
|
+
self.class.module_parent.to_s.demodulize.to_sym
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns the family name
|
|
101
|
+
def family_name
|
|
102
|
+
family_cst.to_s.underscore
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the name of the class constant of this component. Do not override.
|
|
106
|
+
def comp_cst
|
|
107
|
+
self.class.name.demodulize.to_sym
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns the component name
|
|
111
|
+
def comp_name
|
|
112
|
+
comp_cst.to_s.underscore
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @todo deprecate (check for usages beforehand)
|
|
116
|
+
def comp_class_for(...)
|
|
117
|
+
Compony.comp_class_for(...)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @todo deprecate (check for usages beforehand)
|
|
121
|
+
def comp_class_for!(...)
|
|
122
|
+
Compony.comp_class_for!(...)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# DSL method
|
|
126
|
+
def before_render(&block)
|
|
127
|
+
@before_render_block = block
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# DSL method
|
|
131
|
+
# Overrides previous content (also from superclasses). Will be the first content block to run.
|
|
132
|
+
# You can use dyny here.
|
|
133
|
+
def content(&block)
|
|
134
|
+
fail("`content` expects a block in #{inspect}.") unless block_given?
|
|
135
|
+
@content_blocks = [block]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# DSL method
|
|
139
|
+
# Adds a content block that will be executed after all previous ones.
|
|
140
|
+
# It is safe to use this method even if `content` has never been called
|
|
141
|
+
# You can use dyny here.
|
|
142
|
+
def add_content(index = -1, &block)
|
|
143
|
+
fail("`content` expects a block in #{inspect}.") unless block_given?
|
|
144
|
+
@content_blocks ||= []
|
|
145
|
+
@content_blocks.insert(index, block)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Renders the component using the controller passsed to it and returns it as a string.
|
|
149
|
+
# Do not overwrite.
|
|
150
|
+
def render(controller, **locals)
|
|
151
|
+
# Call before_render hook if any and backfire instance variables back to the component
|
|
152
|
+
RequestContext.new(self, controller, locals:).request_context.evaluate_with_backfire(&@before_render_block) if @before_render_block
|
|
153
|
+
# Render, unless before_render has already issued a body (e.g. through redirecting).
|
|
154
|
+
if controller.response.body.blank?
|
|
155
|
+
fail "#{self.class.inspect} must define `content` or set a response body in `before_render`" if @content_blocks.none?
|
|
156
|
+
return controller.render_to_string(
|
|
157
|
+
type: :dyny,
|
|
158
|
+
locals: { content_blocks: @content_blocks, component: self, render_locals: locals },
|
|
159
|
+
inline: <<~RUBY
|
|
160
|
+
content_blocks.each do |block|
|
|
161
|
+
# Instanciate and evaluate a fresh RequestContext in order to use the buffer allocated by the ActionView (needed for `concat` calls)
|
|
162
|
+
Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&block)
|
|
163
|
+
end
|
|
164
|
+
RUBY
|
|
165
|
+
)
|
|
166
|
+
else
|
|
167
|
+
return nil # Prevent double render errors
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# DSL method
|
|
172
|
+
# Adds or replaces an action (for action buttons)
|
|
173
|
+
# If before: is specified, will insert the action before the named action. When replacing, an element keeps its position unless before: is specified.
|
|
174
|
+
def action(action_name, before: nil, &block)
|
|
175
|
+
action_name = action_name.to_sym
|
|
176
|
+
before_name = before&.to_sym
|
|
177
|
+
action = MethodAccessibleHash.new(name: action_name, block:)
|
|
178
|
+
|
|
179
|
+
existing_index = @actions.find_index { |el| el.name == action_name }
|
|
180
|
+
if existing_index.present? && before_name.present?
|
|
181
|
+
@actions.delete_at(existing_index) # Replacing an existing element with a before: directive - must delete before calculating indices
|
|
182
|
+
end
|
|
183
|
+
if before_name.present?
|
|
184
|
+
before_index = @actions.find_index { |el| el.name == before_name } || fail("Action #{before_name} for :before not found in #{inspect}.")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if before_index.present?
|
|
188
|
+
@actions.insert(before_index, action)
|
|
189
|
+
elsif existing_index.present?
|
|
190
|
+
@actions[existing_index] = action
|
|
191
|
+
else
|
|
192
|
+
@actions << action
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# DSL method
|
|
197
|
+
# Marks an action for skip
|
|
198
|
+
def skip_action(action_name)
|
|
199
|
+
@skipped_actions << action_name.to_sym
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Used to render all actions of this component, each button wrapped in a div with the specified class
|
|
203
|
+
def render_actions(controller, wrapper_class: '', action_class: '')
|
|
204
|
+
h = controller.helpers
|
|
205
|
+
h.content_tag(:div, class: wrapper_class) do
|
|
206
|
+
button_htmls = @actions.map do |action|
|
|
207
|
+
next if @skipped_actions.include?(action.name)
|
|
208
|
+
Compony.with_button_defaults(feasibility_action: action.name.to_sym) do
|
|
209
|
+
h.content_tag(:div, action.block.call.render(controller), class: action_class)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
next h.safe_join button_htmls
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Is true for resourceful components
|
|
217
|
+
def resourceful?
|
|
218
|
+
return false
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
protected
|
|
222
|
+
|
|
223
|
+
def check_config!; end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require 'active_support/concern'
|
|
2
|
+
|
|
3
|
+
module Compony
|
|
4
|
+
module ComponentMixins
|
|
5
|
+
module Default
|
|
6
|
+
# This module contains all methods for Component that concern labelling and look
|
|
7
|
+
module Labelling
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
# DSL method and accessor
|
|
11
|
+
# When assigning via DSL, pass format as first parameter.
|
|
12
|
+
# When accessing the value, pass foramt as named parameter
|
|
13
|
+
def label(data_or_format = nil, format: :long, &block)
|
|
14
|
+
format = data_or_format if block_given?
|
|
15
|
+
format ||= :long
|
|
16
|
+
format = format.to_sym
|
|
17
|
+
|
|
18
|
+
if block_given?
|
|
19
|
+
# Assignment via DSL
|
|
20
|
+
if format == :all
|
|
21
|
+
@label_blocks[:short] = block
|
|
22
|
+
@label_blocks[:long] = block
|
|
23
|
+
else
|
|
24
|
+
@label_blocks[format] = block
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
# Retrieval of the actual label
|
|
28
|
+
fail('Label format :all may only be used for setting a label (with a block), not for retrieving it.') if format == :all
|
|
29
|
+
label_block = @label_blocks[format] || fail("Format #{format} was not found for #{inspect}.")
|
|
30
|
+
case label_block.arity
|
|
31
|
+
when 0
|
|
32
|
+
label_block.call
|
|
33
|
+
when 1
|
|
34
|
+
data_or_format ||= data
|
|
35
|
+
if data_or_format.blank?
|
|
36
|
+
fail "Label block of #{inspect} takes an argument, but no data was provided and a call to `data` did not return any data either."
|
|
37
|
+
end
|
|
38
|
+
label_block.call(data_or_format)
|
|
39
|
+
else
|
|
40
|
+
fail "#{inspect} has a label block that takes 2 or more arguments, which is unsupported."
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# DSL method and accessor
|
|
46
|
+
def icon(&block)
|
|
47
|
+
if block_given?
|
|
48
|
+
@icon_block = block
|
|
49
|
+
else
|
|
50
|
+
@icon_block.call
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# DSL method and accessor
|
|
55
|
+
def color(&block)
|
|
56
|
+
if block_given?
|
|
57
|
+
@color_block = block
|
|
58
|
+
else
|
|
59
|
+
@color_block.call
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def init_labelling
|
|
66
|
+
# Provide defaults
|
|
67
|
+
@label_blocks = {
|
|
68
|
+
long: -> { "#{I18n.t(family_name.humanize)}: #{I18n.t(comp_name.humanize)}" },
|
|
69
|
+
short: -> { I18n.t(comp_name.humanize) }
|
|
70
|
+
}
|
|
71
|
+
@icon_block = -> { :'arrow-right' }
|
|
72
|
+
@color_block = -> { :primary }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module ComponentMixins
|
|
3
|
+
module Default
|
|
4
|
+
module Standalone
|
|
5
|
+
class ResourcefulVerbDsl < VerbDsl
|
|
6
|
+
def initialize(...)
|
|
7
|
+
# All resourceful components have a load_data_block, which defaults to the one defined in Resource, defaulting to finding the record.
|
|
8
|
+
@load_data_block = proc { evaluate_with_backfire(&@global_load_data_block) }
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_conf(&)
|
|
13
|
+
return super.deep_merge({
|
|
14
|
+
load_data_block: @load_data_block,
|
|
15
|
+
assign_attributes_block: @assign_attributes_block,
|
|
16
|
+
store_data_block: @store_data_block
|
|
17
|
+
}).compact
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
protected
|
|
21
|
+
|
|
22
|
+
# DSL
|
|
23
|
+
# This is the first step in the life cycle. The block is expected to assign something to `@data`.
|
|
24
|
+
def load_data(&block)
|
|
25
|
+
@load_data_block = block
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# DSL
|
|
29
|
+
# This is called after `load_data`. The block is expected to assign data from `params` as attributes of `@data`.
|
|
30
|
+
# If this method gets never called, the verb config will not contain a assign_attributes block.
|
|
31
|
+
# If called without a block, the verb config will call the global_assign_attributes block defined in Resource.
|
|
32
|
+
def assign_attributes(&block)
|
|
33
|
+
if block_given?
|
|
34
|
+
@assign_attributes_block = block
|
|
35
|
+
else
|
|
36
|
+
@assign_attributes_block = proc { evaluate_with_backfire(&@global_assign_attributes_block) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# DSL
|
|
41
|
+
# This is called after authorization. The block is expected to write back to the database.
|
|
42
|
+
# If this method gets never called, the verb config will not contain a store_data block.
|
|
43
|
+
# If called without a block, the verb config will call the global_store_data block defined in Resource.
|
|
44
|
+
def store_data(&block)
|
|
45
|
+
if block_given?
|
|
46
|
+
@store_data_block = block
|
|
47
|
+
else
|
|
48
|
+
@store_data_block = proc { evaluate_with_backfire(&@global_store_data_block) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module ComponentMixins
|
|
3
|
+
module Default
|
|
4
|
+
module Standalone
|
|
5
|
+
# @api description
|
|
6
|
+
# Wrapper and DSL helper for component's standalone config
|
|
7
|
+
class StandaloneDsl < Dslblend::Base
|
|
8
|
+
def initialize(component, name = nil, path: nil)
|
|
9
|
+
super()
|
|
10
|
+
@component = component
|
|
11
|
+
@name = name&.to_sym
|
|
12
|
+
@path = path
|
|
13
|
+
@verbs = {}
|
|
14
|
+
@skip_authentication = false
|
|
15
|
+
@layout = true # can be overriden by false or a string
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_conf(&block)
|
|
19
|
+
evaluate(&block)
|
|
20
|
+
@component = block.binding.eval('self') # Fetches the component holding this DSL call (via the block)
|
|
21
|
+
return {
|
|
22
|
+
name: @name,
|
|
23
|
+
path: @path,
|
|
24
|
+
verbs: @verbs,
|
|
25
|
+
rails_action_name: Compony.rails_action_name(comp_name, family_name, @name),
|
|
26
|
+
path_helper_name: Compony.path_helper_name(comp_name, family_name, @name),
|
|
27
|
+
skip_authentication: @skip_authentication,
|
|
28
|
+
layout: @layout
|
|
29
|
+
}.compact
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
protected
|
|
33
|
+
|
|
34
|
+
# DSL
|
|
35
|
+
def verb(verb, *args, **nargs, &)
|
|
36
|
+
verb = verb.to_sym
|
|
37
|
+
verb_dsl_class = @component.resourceful? ? ResourcefulVerbDsl : VerbDsl
|
|
38
|
+
@verbs[verb] ||= Compony::MethodAccessibleHash.new
|
|
39
|
+
@verbs[verb].deep_merge! verb_dsl_class.new(@component, verb, *args, **nargs).to_conf(&)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# DSL
|
|
43
|
+
def skip_authentication!
|
|
44
|
+
@skip_authentication = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# DSL
|
|
48
|
+
# Defaults to Rails' default (layouts/application)
|
|
49
|
+
def layout(layout)
|
|
50
|
+
@layout = layout.to_s
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module ComponentMixins
|
|
3
|
+
module Default
|
|
4
|
+
module Standalone
|
|
5
|
+
class VerbDsl < Dslblend::Base
|
|
6
|
+
AVAILABLE_VERBS = %i[get head post put delete connect options trace patch].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(component, verb)
|
|
9
|
+
super()
|
|
10
|
+
|
|
11
|
+
verb = verb.to_sym
|
|
12
|
+
fail "Unknown HTTP verb #{verb.inspect}, use one of #{AVAILABLE_VERBS.inspect}" unless AVAILABLE_VERBS.include?(verb)
|
|
13
|
+
|
|
14
|
+
@component = component
|
|
15
|
+
@verb = verb
|
|
16
|
+
@respond_blocks = { nil => proc { render_standalone(controller) } } # default format
|
|
17
|
+
@authorize_block = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_conf(&)
|
|
21
|
+
evaluate(&) if block_given?
|
|
22
|
+
return {
|
|
23
|
+
verb: @verb,
|
|
24
|
+
authorize_block: @authorize_block || proc { can?(comp_name.to_sym, family_name.to_sym) },
|
|
25
|
+
respond_blocks: @respond_blocks
|
|
26
|
+
}.compact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
protected
|
|
30
|
+
|
|
31
|
+
# DSL
|
|
32
|
+
# This block is expected to return true if and only if current_ability has the right to access the component over the given verb.
|
|
33
|
+
def authorize(&block)
|
|
34
|
+
@authorize_block = block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# DSL
|
|
38
|
+
# This is the last step in the life cycle. It may redirect or render. If omitted, the default is standalone_render.
|
|
39
|
+
# @param format [String, Symbol] Format this block should respond to, defaults to `nil` which means "all other formats".
|
|
40
|
+
def respond(format = nil, &block)
|
|
41
|
+
@respond_blocks[format&.to_sym] = block
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module ComponentMixins
|
|
3
|
+
module Default
|
|
4
|
+
# This contains all default component logic concerning standalone functionality
|
|
5
|
+
module Standalone
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
# Called in routes.rb
|
|
10
|
+
# Returns the compiled standalone config for this component
|
|
11
|
+
# If the components have an inheritance hierarchy, the configs are merged in the right order to perform proper overrides.
|
|
12
|
+
attr_reader :standalone_configs
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Called by fab_controller when a request is issued.
|
|
16
|
+
# This is the entrypoint where a request enters the Component world.
|
|
17
|
+
def on_standalone_access(verb_config, controller)
|
|
18
|
+
# Register as root comp
|
|
19
|
+
if parent_comp.nil?
|
|
20
|
+
fail "#{inspect} is attempting to become root component, but #{root_comp.inspect} is already root." if Compony.root_comp.present?
|
|
21
|
+
RequestStore.store[:compony_root_comp] = self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Prepare the request context in which the innermost DSL calls will be executed
|
|
25
|
+
request_context = RequestContext.new(self, controller)
|
|
26
|
+
|
|
27
|
+
###===---
|
|
28
|
+
# Dispatch request to component. Empty Dslblend base objects are used to provide multiple contexts to the authorize and respond blocks.
|
|
29
|
+
# Lifecycle is (see also "doc/Resourceful Lifecycle.pdf"):
|
|
30
|
+
# - load data (optional, speficied ResourcefulVerbDsl, by convention, should default to the implementation in Resourceful)
|
|
31
|
+
# - after_load_data (optional, specified in Resourceful)
|
|
32
|
+
# - assign_attributes (optional, speficied ResourcefulVerbDsl, by convention, should default to the implementation in Resourceful)
|
|
33
|
+
# - after_assign_attributes (optional, specified in Resourceful)
|
|
34
|
+
# - authorize
|
|
35
|
+
# - store_data (optional, speficied ResourcefulVerbDsl, by convention, should default to the implementation in Resourceful)
|
|
36
|
+
# - respond (typically either redirect or render standalone, specified in VerbDsl), which defaults to render_standalone, performing:
|
|
37
|
+
# - before_render
|
|
38
|
+
# - render (unless before_render already redirected)
|
|
39
|
+
###===---
|
|
40
|
+
|
|
41
|
+
if verb_config.load_data_block
|
|
42
|
+
request_context.evaluate_with_backfire(&verb_config.load_data_block)
|
|
43
|
+
if global_after_load_data_block
|
|
44
|
+
request_context.evaluate_with_backfire(&global_after_load_data_block)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if verb_config.assign_attributes_block
|
|
49
|
+
request_context.evaluate_with_backfire(&verb_config.assign_attributes_block)
|
|
50
|
+
if global_after_assign_attributes_block
|
|
51
|
+
request_context.evaluate_with_backfire(&global_after_assign_attributes_block)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# TODO: Make much prettier, providing message, action, subject and conditions
|
|
56
|
+
fail CanCan::AccessDenied, inspect unless request_context.evaluate(&verb_config.authorize_block)
|
|
57
|
+
|
|
58
|
+
if verb_config.store_data_block
|
|
59
|
+
request_context.evaluate_with_backfire(&verb_config.store_data_block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if there is a specific respond block for the format.
|
|
63
|
+
# If there isn't, fallback to the nil respond block, which defaults to `render_standalone`.
|
|
64
|
+
respond_block = verb_config.respond_blocks[controller.request.format.symbol] || verb_config.respond_blocks[nil]
|
|
65
|
+
request_context.evaluate(&respond_block)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Call this on a standalone component to find out whether default GET access is permitted for the current user.
|
|
69
|
+
# This is useful to hide/disable buttons leading to components a user may not press.
|
|
70
|
+
# For resourceful components, before calling this, you must have loaded date beforehand, for instance in one of the following ways:
|
|
71
|
+
# - when called standalone (via request to the component), the load data step must be completed
|
|
72
|
+
# - when called to check for permission only, e.g. to display a button to it, initialize the component by passing the :data keyword to `new`
|
|
73
|
+
# By default, this checks the authorization to access the main standalone entrypoint (with name `nil`) and HTTP verb GET.
|
|
74
|
+
def standalone_access_permitted_for?(controller, standalone_name: nil, verb: :get)
|
|
75
|
+
standalone_name = standalone_name&.to_sym
|
|
76
|
+
verb = verb.to_sym
|
|
77
|
+
standalone_config = standalone_configs[standalone_name] || fail("#{inspect} does not provide the standalone config #{standalone_config.inspect}.")
|
|
78
|
+
verb = standalone_config.verbs[verb] || fail("#{inspect} standalone config #{standalone_config.inspect} does not provide verb #{verb.inspect}.")
|
|
79
|
+
return RequestContext.new(self, controller).evaluate(&verb.authorize_block)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Renders the component using the controller passed to it upon instanciation (calls the controller's render)
|
|
83
|
+
# Do not overwrite
|
|
84
|
+
def render_standalone(controller, status: nil, standalone_name: nil)
|
|
85
|
+
# Start the render process. This produces a nil value if before_render has already produced a response, e.g. a redirect.
|
|
86
|
+
rendered_html = render(controller)
|
|
87
|
+
if rendered_html.present? # If nil, a response body was already produced in the controller and we take no action here (would have DoubleRenderError)
|
|
88
|
+
opts = { html: rendered_html, layout: @standalone_configs[standalone_name].layout }
|
|
89
|
+
opts[:status] = status if status.present?
|
|
90
|
+
controller.respond_to do |format|
|
|
91
|
+
# Form posts trigger format types turbo stream and then html, turbo stream wins.
|
|
92
|
+
# For this reason, Rails prefers stream, in which case the layout is disabled, regardless of the option.
|
|
93
|
+
# To mitigate this, we use respond_to to force a HTML-only response.
|
|
94
|
+
format.html { controller.render(**opts) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
protected
|
|
100
|
+
|
|
101
|
+
# DSL method
|
|
102
|
+
def standalone(name = nil, *args, **nargs, &block)
|
|
103
|
+
block = proc {} unless block_given? # If called without a block, must default to an empty block to provide a binding to the DSL.
|
|
104
|
+
name = name&.to_sym # nil name is the most common case
|
|
105
|
+
@standalone_configs[name] ||= Compony::MethodAccessibleHash.new
|
|
106
|
+
@standalone_configs[name].deep_merge! StandaloneDsl.new(self, name, *args, **nargs).to_conf(&block)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def init_standalone
|
|
112
|
+
@standalone_configs = {}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module ComponentMixins
|
|
3
|
+
# Include this when your component's family name corresponds to the pluralized Rails model name the component's family is responsible for.
|
|
4
|
+
# When including this, the component gets an attribute @data which contains a record or a collection of records.
|
|
5
|
+
# Resourceful components are always aware of a data_class, corresponding to the expected @data.class and used e.g. to render lists or for `.new`.
|
|
6
|
+
module Resourceful
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
attr_reader :data
|
|
10
|
+
|
|
11
|
+
# Must prefix the following instance variables with global_ in order to avoid overwriting VerbDsl inst vars due to Dslblend.
|
|
12
|
+
attr_reader :global_load_data_block
|
|
13
|
+
attr_reader :global_after_load_data_block
|
|
14
|
+
attr_reader :global_assign_attributes_block
|
|
15
|
+
attr_reader :global_after_assign_attributes_block
|
|
16
|
+
attr_reader :global_store_data_block
|
|
17
|
+
|
|
18
|
+
def initialize(*args, data: nil, data_class: nil, **nargs, &block)
|
|
19
|
+
@data = data
|
|
20
|
+
@data_class = data_class
|
|
21
|
+
|
|
22
|
+
# Provide defaults for hook blocks
|
|
23
|
+
@global_load_data_block ||= proc { @data = self.data_class.find(controller.params[:id]) }
|
|
24
|
+
|
|
25
|
+
super(*args, **nargs, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# DSL method
|
|
29
|
+
# Sets or calculates the model class based on the component's family name
|
|
30
|
+
def data_class(new_data_class = nil)
|
|
31
|
+
@data_class ||= new_data_class || family_cst.to_s.singularize.constantize
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Instanciate a component with `self` as a parent and render it, having it inherit the resource
|
|
35
|
+
def resourceful_sub_comp(component_class, **comp_opts)
|
|
36
|
+
comp_opts[:data] ||= data # Inject additional param before forwarding all of them to super
|
|
37
|
+
comp_opts[:data_class] ||= data_class # Inject additional param before forwarding all of them to super
|
|
38
|
+
sub_comp(component_class, **comp_opts)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def resourceful?
|
|
42
|
+
return true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
# DSL method
|
|
48
|
+
# Sets a default load_data block for all standalone paths and verbs.
|
|
49
|
+
# Can be overwritten for a specific path and verb in the
|
|
50
|
+
# {Compony::ComponentMixins::Default::Standalone::VerbDsl}.
|
|
51
|
+
# The block is expected to assign `@data`.
|
|
52
|
+
# @see Compony::ComponentMixins::Default::Standalone::VerbDsl#load_data
|
|
53
|
+
def load_data(&block)
|
|
54
|
+
@global_load_data_block = block
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# DSL method
|
|
58
|
+
# Runs after loading data and before authorization for all standalone paths and verbs.
|
|
59
|
+
# Example use case: if `load_data` produced an AR collection proxy, can still refine result here before `to_sql` is called.
|
|
60
|
+
def after_load_data(&block)
|
|
61
|
+
@global_after_load_data_block = block
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# DSL method
|
|
65
|
+
# Sets a default default assign_attributes block for all standalone paths and verbs.
|
|
66
|
+
# Can be overwritten for a specific path and verb in the
|
|
67
|
+
# {Compony::ComponentMixins::Default::Standalone::VerbDsl}.
|
|
68
|
+
# The block is expected to assign suitable `params` to attributes of `@data`.
|
|
69
|
+
# @see Compony::ComponentMixins::Default::Standalone::VerbDsl#assign_attributes
|
|
70
|
+
def assign_attributes(&block)
|
|
71
|
+
@global_assign_attributes_block = block
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# DSL method
|
|
75
|
+
# Runs after `assign_attributes` and before `store_data` for all standalone paths and verbs.
|
|
76
|
+
# Example use case: prefilling some fields for a form
|
|
77
|
+
def after_assign_attributes(&block)
|
|
78
|
+
@global_after_assign_attributes_block = block
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# DSL method
|
|
82
|
+
# Sets a default store_data block for all standalone paths and verbs.
|
|
83
|
+
# Can be overwritten for a specific path and verb in the
|
|
84
|
+
# {Compony::ComponentMixins::Default::Standalone::VerbDsl}.
|
|
85
|
+
# The block is expected save `@data` to the database.
|
|
86
|
+
# @see Compony::ComponentMixins::Default::Standalone::VerbDsl#store_data
|
|
87
|
+
def store_data(&block)
|
|
88
|
+
@global_store_data_block = block
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|