compony 0.7.1 → 0.8.0
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 +4 -4
- data/CHANGELOG.md +34 -0
- data/Gemfile.lock +1 -1
- data/README.md +10 -14
- data/VERSION +1 -1
- data/compony.gemspec +4 -4
- data/doc/ComponentGenerator.html +1 -1
- data/doc/Components.html +1 -1
- data/doc/ComponentsGenerator.html +1 -1
- data/doc/Compony/Component.html +193 -457
- data/doc/Compony/ComponentMixins/Default/Labelling.html +1 -1
- data/doc/Compony/ComponentMixins/Default/Standalone/ResourcefulVerbDsl.html +1 -1
- data/doc/Compony/ComponentMixins/Default/Standalone/StandaloneDsl.html +3 -3
- data/doc/Compony/ComponentMixins/Default/Standalone/VerbDsl.html +1 -1
- data/doc/Compony/ComponentMixins/Default/Standalone.html +187 -1
- data/doc/Compony/ComponentMixins/Default.html +1 -1
- data/doc/Compony/ComponentMixins/Resourceful.html +2 -2
- data/doc/Compony/ComponentMixins.html +1 -1
- data/doc/Compony/Components/Button.html +2 -2
- data/doc/Compony/Components/Buttons/CssButton.html +282 -0
- data/doc/Compony/Components/Buttons/Link.html +252 -0
- data/doc/Compony/Components/Buttons.html +126 -0
- data/doc/Compony/Components/Destroy.html +11 -11
- data/doc/Compony/Components/Edit.html +14 -14
- data/doc/Compony/Components/Form.html +100 -100
- data/doc/Compony/Components/Index.html +2 -2
- data/doc/Compony/Components/List.html +3 -3
- data/doc/Compony/Components/New.html +2 -2
- data/doc/Compony/Components/Show.html +24 -24
- data/doc/Compony/Components/WithForm.html +3 -3
- data/doc/Compony/Components.html +5 -3
- data/doc/Compony/ControllerMixin.html +2 -2
- data/doc/Compony/Engine.html +1 -1
- data/doc/Compony/ExposedIntentsDsl.html +403 -0
- data/doc/Compony/Intent.html +1503 -0
- data/doc/Compony/MethodAccessibleHash.html +1 -1
- data/doc/Compony/ModelFields/Anchormodel.html +1 -1
- data/doc/Compony/ModelFields/Association.html +2 -2
- data/doc/Compony/ModelFields/Attachment.html +1 -1
- data/doc/Compony/ModelFields/Base.html +1 -1
- data/doc/Compony/ModelFields/Boolean.html +1 -1
- data/doc/Compony/ModelFields/Color.html +1 -1
- data/doc/Compony/ModelFields/Currency.html +1 -1
- data/doc/Compony/ModelFields/Date.html +1 -1
- data/doc/Compony/ModelFields/Datetime.html +1 -1
- data/doc/Compony/ModelFields/Decimal.html +1 -1
- data/doc/Compony/ModelFields/Email.html +1 -1
- data/doc/Compony/ModelFields/Float.html +1 -1
- data/doc/Compony/ModelFields/Integer.html +1 -1
- data/doc/Compony/ModelFields/Percentage.html +1 -1
- data/doc/Compony/ModelFields/Phone.html +1 -1
- data/doc/Compony/ModelFields/RichText.html +1 -1
- data/doc/Compony/ModelFields/String.html +1 -1
- data/doc/Compony/ModelFields/Text.html +1 -1
- data/doc/Compony/ModelFields/Time.html +1 -1
- data/doc/Compony/ModelFields/Url.html +1 -1
- data/doc/Compony/ModelFields.html +1 -1
- data/doc/Compony/ModelMixin.html +1 -1
- data/doc/Compony/NaturalOrdering.html +1 -1
- data/doc/Compony/RequestContext.html +177 -14
- data/doc/Compony/Version.html +1 -1
- data/doc/Compony/ViewHelpers.html +15 -272
- data/doc/Compony/VirtualModel.html +1 -1
- data/doc/Compony.html +303 -837
- data/doc/ComponyController.html +1 -1
- data/doc/_index.html +30 -2
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +11 -18
- data/doc/guide/basic_component.md +12 -8
- data/doc/guide/example.md +17 -17
- data/doc/guide/feasibility.md +4 -2
- data/doc/guide/generators.md +4 -2
- data/doc/guide/inheritance.md +4 -2
- data/doc/guide/installation.md +4 -2
- data/doc/guide/intents.md +167 -0
- data/doc/guide/internal_datastructures.md +4 -2
- data/doc/guide/model_fields.md +4 -2
- data/doc/guide/nesting.md +5 -3
- data/doc/guide/ownership.md +5 -3
- data/doc/guide/pre_built_components/destroy.md +3 -3
- data/doc/guide/pre_built_components/edit.md +1 -1
- data/doc/guide/pre_built_components/form.md +1 -1
- data/doc/guide/pre_built_components/index.md +1 -1
- data/doc/guide/pre_built_components/list.md +1 -1
- data/doc/guide/pre_built_components/new.md +2 -2
- data/doc/guide/pre_built_components/show.md +1 -1
- data/doc/guide/pre_built_components/with_form.md +1 -1
- data/doc/guide/pre_built_components.md +4 -3
- data/doc/guide/resourceful.md +5 -3
- data/doc/guide/standalone.md +10 -2
- data/doc/guide/virtual_models.md +4 -2
- data/doc/index.html +11 -18
- data/doc/method_list.html +273 -161
- data/doc/top-level-namespace.html +1 -1
- data/lib/compony/component.rb +19 -48
- data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +2 -2
- data/lib/compony/component_mixins/default/standalone.rb +16 -0
- data/lib/compony/component_mixins/resourceful.rb +1 -1
- data/lib/compony/components/buttons/css_button.rb +32 -0
- data/lib/compony/components/buttons/link.rb +31 -0
- data/lib/compony/components/destroy.rb +9 -8
- data/lib/compony/components/edit.rb +5 -4
- data/lib/compony/components/form.rb +7 -1
- data/lib/compony/components/index.rb +2 -2
- data/lib/compony/components/list.rb +4 -4
- data/lib/compony/components/new.rb +1 -1
- data/lib/compony/components/show.rb +8 -11
- data/lib/compony/components/with_form.rb +1 -1
- data/lib/compony/exposed_intents_dsl.rb +29 -0
- data/lib/compony/intent.rb +145 -0
- data/lib/compony/model_fields/association.rb +1 -1
- data/lib/compony/request_context.rb +21 -0
- data/lib/compony/view_helpers.rb +5 -48
- data/lib/compony.rb +63 -149
- metadata +12 -6
- data/doc/guide/helpers.md +0 -156
- data/doc/guide/pre_built_components/button.md +0 -8
- data/doc/guide/root_actions.md +0 -67
- data/lib/compony/components/button.rb +0 -61
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
</div>
|
|
103
103
|
|
|
104
104
|
<div id="footer">
|
|
105
|
-
Generated on Thu Nov
|
|
105
|
+
Generated on Thu Nov 27 16:02:21 2025 by
|
|
106
106
|
<a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
107
107
|
0.9.34 (ruby-3.3.5).
|
|
108
108
|
</div>
|
data/lib/compony/component.rb
CHANGED
|
@@ -20,24 +20,14 @@ module Compony
|
|
|
20
20
|
setup_blocks << block
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Returns the name of the module constant (=family) of this component. Do not override.
|
|
24
|
-
def self.family_cst
|
|
25
|
-
module_parent.to_s.demodulize.to_sym
|
|
26
|
-
end
|
|
27
|
-
|
|
28
23
|
# Returns the family name
|
|
29
24
|
def self.family_name
|
|
30
|
-
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Returns the name of the class constant of this component. Do not override.
|
|
34
|
-
def self.comp_cst
|
|
35
|
-
name.demodulize.to_sym
|
|
25
|
+
module_parent.to_s.demodulize.underscore
|
|
36
26
|
end
|
|
37
27
|
|
|
38
28
|
# Returns the component name
|
|
39
29
|
def self.comp_name
|
|
40
|
-
|
|
30
|
+
name.demodulize.underscore
|
|
41
31
|
end
|
|
42
32
|
|
|
43
33
|
def initialize(parent_comp = nil, index: 0, **comp_opts)
|
|
@@ -48,11 +38,12 @@ module Compony
|
|
|
48
38
|
@before_render_blocks = NaturalOrdering.new
|
|
49
39
|
@content_blocks = NaturalOrdering.new
|
|
50
40
|
@actions = NaturalOrdering.new
|
|
41
|
+
@exposed_intent_blocks = []
|
|
51
42
|
@skipped_actions = Set.new
|
|
52
43
|
@path_block = proc do |model = nil, *args_for_path_helper, standalone_name: nil, **kwargs_for_path_helper|
|
|
53
44
|
kwargs_for_path_helper.merge!(id: model.id) if model
|
|
54
45
|
next Rails.application.routes.url_helpers.send(
|
|
55
|
-
"#{
|
|
46
|
+
"#{path_helper_name(standalone_name)}_path",
|
|
56
47
|
*args_for_path_helper,
|
|
57
48
|
**kwargs_for_path_helper
|
|
58
49
|
)
|
|
@@ -114,21 +105,16 @@ module Compony
|
|
|
114
105
|
end
|
|
115
106
|
|
|
116
107
|
# Instanciate a component with `self` as a parent
|
|
117
|
-
def sub_comp(
|
|
118
|
-
|
|
108
|
+
def sub_comp(*, **comp_opts)
|
|
109
|
+
intent = Compony.intent(*)
|
|
110
|
+
sub = intent.comp(self, index: @sub_comps.count, **comp_opts)
|
|
119
111
|
@sub_comps << sub
|
|
120
112
|
return sub
|
|
121
113
|
end
|
|
122
114
|
|
|
123
|
-
# Returns the name of the module constant (=family) of this component. Do not override.
|
|
124
|
-
delegate :family_cst, to: :class
|
|
125
|
-
|
|
126
115
|
# Returns the family name
|
|
127
116
|
delegate :family_name, to: :class
|
|
128
117
|
|
|
129
|
-
# Returns the name of the class constant of this component. Do not override.
|
|
130
|
-
delegate :comp_cst, to: :class
|
|
131
|
-
|
|
132
118
|
# Returns the component name
|
|
133
119
|
delegate :comp_name, to: :class
|
|
134
120
|
|
|
@@ -227,33 +213,18 @@ module Compony
|
|
|
227
213
|
end
|
|
228
214
|
|
|
229
215
|
# DSL method
|
|
230
|
-
#
|
|
231
|
-
# If
|
|
232
|
-
def
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
# Used to render all actions of this component, each button wrapped in a div with the specified class
|
|
243
|
-
def render_actions(controller, wrapper_class: '', action_class: '')
|
|
244
|
-
h = controller.helpers
|
|
245
|
-
h.content_tag(:div, class: wrapper_class) do
|
|
246
|
-
button_htmls = @actions.map do |action|
|
|
247
|
-
next if @skipped_actions.include?(action.name)
|
|
248
|
-
Compony.with_button_defaults(feasibility_action: action.name.to_sym) do
|
|
249
|
-
action_button = action.payload.call(controller)
|
|
250
|
-
next unless action_button
|
|
251
|
-
button_html = action_button.render(controller)
|
|
252
|
-
next if button_html.blank?
|
|
253
|
-
h.content_tag(:div, button_html, class: action_class)
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
next h.safe_join button_htmls
|
|
216
|
+
# If a block is given: Enters the DSL where exposed intents can be added or removed (use from {Component#setup} within the component).
|
|
217
|
+
# If no block is given: Builds the declared intents and returns them (use from a {RequestContest} outside the component).
|
|
218
|
+
def exposed_intents(&block)
|
|
219
|
+
if block_given?
|
|
220
|
+
# Enter DSL
|
|
221
|
+
@exposed_intent_blocks << block
|
|
222
|
+
else
|
|
223
|
+
# Build the declared intents
|
|
224
|
+
return @exposed_intents if @exposed_intents
|
|
225
|
+
@exposed_intents = NaturalOrdering.new
|
|
226
|
+
@exposed_intent_blocks.each { |block| ExposedIntentsDsl.new(@exposed_intents).evaluate(&block) } # alters @exposed_intents
|
|
227
|
+
return @exposed_intents.map!(&:payload)
|
|
257
228
|
end
|
|
258
229
|
end
|
|
259
230
|
|
|
@@ -32,8 +32,8 @@ module Compony
|
|
|
32
32
|
scope: @scope,
|
|
33
33
|
scope_args: @scope_args,
|
|
34
34
|
verbs: @verbs,
|
|
35
|
-
rails_action_name:
|
|
36
|
-
path_helper_name:
|
|
35
|
+
rails_action_name: rails_action_name(@name),
|
|
36
|
+
path_helper_name: path_helper_name(@name),
|
|
37
37
|
skip_authentication: @skip_authentication,
|
|
38
38
|
skip_forgery_protection: @skip_forgery_protection,
|
|
39
39
|
layout: @layout
|
|
@@ -123,6 +123,22 @@ module Compony
|
|
|
123
123
|
@standalone_configs = {}
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
# Returns the name of the ComponyController action for this component.<br>
|
|
127
|
+
# Optionally can pass a name for extra standalone configs.
|
|
128
|
+
# @param name [String,Symbol] If referring to an extra standalone entrypoint, specify its name using this param.
|
|
129
|
+
# @see Compony#path
|
|
130
|
+
def rails_action_name(name = nil)
|
|
131
|
+
[name.presence, comp_name, family_name].compact.join('_')
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns the name of the Rails URL helper returning the path to this component.<br>
|
|
135
|
+
# Optionally can pass a name for extra standalone configs.
|
|
136
|
+
# @param name [String,Symbol] If referring to an extra standalone entrypoint, specify its name using this param.
|
|
137
|
+
# @see Compony#path
|
|
138
|
+
def path_helper_name(...)
|
|
139
|
+
"#{rails_action_name(...)}_comp"
|
|
140
|
+
end
|
|
141
|
+
|
|
126
142
|
private
|
|
127
143
|
|
|
128
144
|
def init_standalone
|
|
@@ -28,7 +28,7 @@ module Compony
|
|
|
28
28
|
# DSL method
|
|
29
29
|
# Sets or calculates the model class based on the component's family name
|
|
30
30
|
def data_class(new_data_class = nil)
|
|
31
|
-
@data_class ||= new_data_class ||
|
|
31
|
+
@data_class ||= new_data_class || family_name.singularize.camelize.constantize
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Instanciate a component with `self` as a parent and render it, having it inherit the resource
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module Components
|
|
3
|
+
module Buttons
|
|
4
|
+
class CssButton < Link
|
|
5
|
+
def prepare_opts!
|
|
6
|
+
super
|
|
7
|
+
css = [
|
|
8
|
+
'display:inline-block',
|
|
9
|
+
'padding:.15rem .35rem',
|
|
10
|
+
'text-decoration:none',
|
|
11
|
+
'border-radius:6px',
|
|
12
|
+
'font-size: 13.333px'
|
|
13
|
+
]
|
|
14
|
+
if @comp_opts[:class]&.split&.include?('disabled')
|
|
15
|
+
css += [
|
|
16
|
+
'background:#e9ecef',
|
|
17
|
+
'color:#6c757d',
|
|
18
|
+
'cursor: default'
|
|
19
|
+
]
|
|
20
|
+
else
|
|
21
|
+
css += [
|
|
22
|
+
'background:#eaeaee',
|
|
23
|
+
'color:#000000',
|
|
24
|
+
'border:1px solid #90909e'
|
|
25
|
+
]
|
|
26
|
+
end
|
|
27
|
+
@comp_opts[:style] = "#{css.join(';')};"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module Components
|
|
3
|
+
module Buttons
|
|
4
|
+
class Link < Component
|
|
5
|
+
setup do
|
|
6
|
+
before_render do
|
|
7
|
+
prepare_opts!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
content do
|
|
11
|
+
concat link_to(@label, @href, **@comp_opts)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
protected
|
|
16
|
+
|
|
17
|
+
def prepare_opts!
|
|
18
|
+
@label = @comp_opts.delete(:label).presence
|
|
19
|
+
@href = @comp_opts.delete(:href).presence || 'javascript:void(0)'
|
|
20
|
+
@method = @comp_opts.delete(:method).presence
|
|
21
|
+
if @method && @method.to_sym != :get
|
|
22
|
+
@comp_opts[:data] = { turbo_method: @method }.merge(@comp_opts[:data] || {})
|
|
23
|
+
end
|
|
24
|
+
if @comp_opts[:class]&.split&.include?('disabled')
|
|
25
|
+
@comp_opts[:style] = 'text-decoration:line-through;cursor: default;color: #6c757d;'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -32,10 +32,10 @@ module Compony
|
|
|
32
32
|
|
|
33
33
|
content :confirm_button, hidden: true do
|
|
34
34
|
div do
|
|
35
|
-
concat
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
concat render_intent(comp_name,
|
|
36
|
+
@data,
|
|
37
|
+
label: I18n.t('compony.components.destroy.confirm_button'),
|
|
38
|
+
method: :delete)
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -44,9 +44,10 @@ module Compony
|
|
|
44
44
|
content :confirm_button
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
exposed_intents do
|
|
48
|
+
if data_class.owner_model_attr
|
|
49
|
+
add :show, @data.send(data_class.owner_model_attr), icon: :xmark, color: :secondary, label: I18n.t('compony.cancel'), name: :back_to_owner
|
|
50
|
+
end
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
store_data do
|
|
@@ -72,7 +73,7 @@ module Compony
|
|
|
72
73
|
if data_class.owner_model_attr.present?
|
|
73
74
|
Compony.path(:show, @data.send(data_class.owner_model_attr))
|
|
74
75
|
else
|
|
75
|
-
Compony.path(:index,
|
|
76
|
+
Compony.path(:index, family_name)
|
|
76
77
|
end
|
|
77
78
|
end
|
|
78
79
|
end
|
|
@@ -33,9 +33,10 @@ module Compony
|
|
|
33
33
|
|
|
34
34
|
form_cancancan_action :edit
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
exposed_intents do
|
|
37
|
+
if data_class.owner_model_attr
|
|
38
|
+
add :show, @data.send(data_class.owner_model_attr), icon: :xmark, color: :secondary, label: I18n.t('compony.cancel'), name: :back_to_owner
|
|
39
|
+
end
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
content :label do
|
|
@@ -78,7 +79,7 @@ module Compony
|
|
|
78
79
|
elsif data_class.owner_model_attr.present?
|
|
79
80
|
Compony.path(:show, @data.send(data_class.owner_model_attr))
|
|
80
81
|
else
|
|
81
|
-
Compony.path(:index,
|
|
82
|
+
Compony.path(:index, family_name)
|
|
82
83
|
end
|
|
83
84
|
end
|
|
84
85
|
|
|
@@ -25,9 +25,15 @@ module Compony
|
|
|
25
25
|
|
|
26
26
|
# Override this to provide a custom submit button
|
|
27
27
|
content :submit_button, hidden: true do
|
|
28
|
+
# Fake submit button rendered by a button component and submitting the form via JS:
|
|
28
29
|
concat Compony.button_component_class.new(
|
|
29
|
-
label:
|
|
30
|
+
label: @submit_label || I18n.t('compony.components.form.submit'),
|
|
31
|
+
icon: 'arrow-right',
|
|
32
|
+
href: '#',
|
|
33
|
+
onclick: "this.closest('form').requestSubmit(); return false;"
|
|
30
34
|
).render(controller)
|
|
35
|
+
# Real (but hidden) submit button to allow Return to submit:
|
|
36
|
+
button type: :submit, hidden: true
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
# Override this to provide additional submit buttons.
|
|
@@ -18,9 +18,9 @@ module Compony
|
|
|
18
18
|
@data = data_class.accessible_by(controller.current_ability)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
exposed_intents do
|
|
22
22
|
if Compony.comp_class_for(:new, data_class)
|
|
23
|
-
|
|
23
|
+
add :new, data_class.model_name.plural, name: :new
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -168,7 +168,7 @@ module Compony
|
|
|
168
168
|
unless block_given?
|
|
169
169
|
block = proc do |record|
|
|
170
170
|
next if Compony.comp_class_for(name, record).nil?
|
|
171
|
-
|
|
171
|
+
render_intent(name, record, **{ label: { format: :short } }.deep_merge(button_opts))
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
@row_actions.natural_push(name, block, **)
|
|
@@ -275,9 +275,9 @@ module Compony
|
|
|
275
275
|
end
|
|
276
276
|
|
|
277
277
|
# Default row actions (use override or skip_row_action to prevent)
|
|
278
|
-
row_action(:show)
|
|
279
|
-
row_action(:edit)
|
|
280
|
-
row_action(:destroy)
|
|
278
|
+
row_action(:show) if Compony.comp_class_for(:show, data_class)
|
|
279
|
+
row_action(:edit) if Compony.comp_class_for(:edit, data_class)
|
|
280
|
+
row_action(:destroy) if Compony.comp_class_for(:destroy, data_class)
|
|
281
281
|
|
|
282
282
|
before_render do
|
|
283
283
|
process_data!(controller)
|
|
@@ -16,20 +16,17 @@ module Compony
|
|
|
16
16
|
label(:short) { |_| I18n.t('compony.components.show.label.short') }
|
|
17
17
|
icon { :eye }
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
exposed_intents do
|
|
20
|
+
if data_class.owner_model_attr
|
|
21
|
+
add :show, @data.send(data_class.owner_model_attr), icon: :xmark, color: :secondary, label: I18n.t('compony.back'), name: :back_to_owner
|
|
22
|
+
end
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Compony.button(:edit, @data, label_opts: { format: :short })
|
|
24
|
+
if Compony.comp_class_for(:edit, family_name)
|
|
25
|
+
add :edit, @data, label: { format: :short }, name: :edit
|
|
27
26
|
end
|
|
28
|
-
end
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Compony.button(:destroy, @data, label_opts: { format: :short })
|
|
28
|
+
if Compony.comp_class_for(:destroy, family_name)
|
|
29
|
+
add :destroy, @data, label: { format: :short }, name: :destroy
|
|
33
30
|
end
|
|
34
31
|
end
|
|
35
32
|
|
|
@@ -13,7 +13,7 @@ module Compony
|
|
|
13
13
|
# Returns an instance of the form component responsible for rendering the form.
|
|
14
14
|
# Feel free to override this in subclasses.
|
|
15
15
|
def form_comp
|
|
16
|
-
@form_comp ||= (form_comp_class || Compony.comp_class_for!(:form,
|
|
16
|
+
@form_comp ||= (form_comp_class || Compony.comp_class_for!(:form, family_name)).new(
|
|
17
17
|
self,
|
|
18
18
|
submit_verb:,
|
|
19
19
|
# If applicable, Rails adds the route keys automatically, thus, e.g. :id does not need to be passed here, as it comes from the request.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
class ExposedIntentsDsl < Dslblend::Base
|
|
3
|
+
def initialize(previously_exposed_intents)
|
|
4
|
+
super()
|
|
5
|
+
@exposed_intents = previously_exposed_intents
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
protected
|
|
9
|
+
|
|
10
|
+
# DSL method
|
|
11
|
+
# Adds or replaces an intent to those exposed by the component based on the intent name (override the name if you need to avoid a naming collision).
|
|
12
|
+
# Intents specified this way can be retrieved and rendered by the parent component or by calling `root_intents` in case of standalone access.
|
|
13
|
+
# @param [Symbol] before If specified, will insert the intent before the other. When replacing, an element keeps its position unless `before:`` is passed.
|
|
14
|
+
def add(*, before: nil, **)
|
|
15
|
+
intent = Compony.intent(*, **)
|
|
16
|
+
@exposed_intents.natural_push(intent.name, intent, before:)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# DSL method
|
|
20
|
+
# Removes an exposed intent previously added to this component
|
|
21
|
+
# @param [Symbol] intent_name The name of the intent to remove
|
|
22
|
+
def remove(intent_name)
|
|
23
|
+
existing_index = @exposed_intents.find_index { |el| el.name == intent_name.to_sym }
|
|
24
|
+
if existing_index
|
|
25
|
+
@exposed_intents.delete_at(existing_index)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
# @api description
|
|
3
|
+
# An Intent is a a gateway to a component, along with relevant context, such as the comp and family, perhaps a resource, standalone name, feasibility etc.
|
|
4
|
+
# The class provides tooling used by various Compony helpers used to point to other components in some way.
|
|
5
|
+
class Intent
|
|
6
|
+
attr_reader :comp_class
|
|
7
|
+
attr_reader :data
|
|
8
|
+
|
|
9
|
+
# @param comp_name_or_cst_or_class [String,Symbol,Class] The component that should be loaded,
|
|
10
|
+
# for instance `ShowForAll`, `'ShowForAll'` or `:show_for_all`,
|
|
11
|
+
# or can also pass a component class (such as Components::Users::Show)
|
|
12
|
+
# @param model_or_family_name_or_cst [String,Symbol,ApplicationRecord] Either the family that contains the requested component, or an
|
|
13
|
+
# instance implementing `model_name` from which the family name is auto-generated.
|
|
14
|
+
# Examples: `Users`, `'Users'`, `:users`, `User.first`
|
|
15
|
+
# @param standalone_name [Symbol] If given, will override the standalone name for all `path` calls for this intent instance.
|
|
16
|
+
# @param name [Symbol] If given, will override the name of this intent. Defaults to component and family name joined by underscore.
|
|
17
|
+
# @param label [String,Hash] If given, will be used for generating the label. If Hash, is given as options to {Intent#label}.
|
|
18
|
+
# @param path [String,Hash] If given, will be used for generating the path. If Hash, is given as options to {Intent#path}.
|
|
19
|
+
# @param data [ApplicationRecord,Object] If given, the target component will be instanciated with this argument. Omit if your second pos arg is a model.
|
|
20
|
+
# @param data_class [Class] If given, the target component will be instanciated with this argument.
|
|
21
|
+
# @param feasibility_target [ApplicationRecord] If given, will override the feasibility target (prevention framework)
|
|
22
|
+
# @param feasibility_action [ApplicationRecord] If given, will override the feasibility action (prevention framework)
|
|
23
|
+
def initialize(comp_name_or_cst_or_class,
|
|
24
|
+
model_or_family_name_or_cst = nil,
|
|
25
|
+
standalone_name: nil,
|
|
26
|
+
name: nil,
|
|
27
|
+
label: nil,
|
|
28
|
+
path: nil,
|
|
29
|
+
method: nil,
|
|
30
|
+
data: nil,
|
|
31
|
+
data_class: nil,
|
|
32
|
+
feasibility_target: nil,
|
|
33
|
+
feasibility_action: nil,
|
|
34
|
+
**custom_args)
|
|
35
|
+
# Check for model / data
|
|
36
|
+
@data = data
|
|
37
|
+
@data ||= model_or_family_name_or_cst if model_or_family_name_or_cst.respond_to?(:model_name)
|
|
38
|
+
@data_class = data_class
|
|
39
|
+
|
|
40
|
+
# Figure out comp_class
|
|
41
|
+
if comp_name_or_cst_or_class.is_a?(Class) && (comp_name_or_cst_or_class <= Compony::Component)
|
|
42
|
+
# A class was given as the first argument
|
|
43
|
+
@comp_class = comp_name_or_cst_or_class
|
|
44
|
+
else
|
|
45
|
+
# Build the constant from the first two arguments
|
|
46
|
+
family_underscore_str = @data.respond_to?(:model_name) ? @data.model_name.plural : model_or_family_name_or_cst.to_s.underscore
|
|
47
|
+
constant_str = "::Components::#{family_underscore_str.camelize}::#{comp_name_or_cst_or_class.to_s.camelize}"
|
|
48
|
+
@comp_class = constant_str.constantize
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Store further arguments
|
|
52
|
+
@name = name&.to_sym
|
|
53
|
+
@standalone_name = standalone_name
|
|
54
|
+
@label = label.is_a?(String) ? label : nil
|
|
55
|
+
@label_opts = label.is_a?(Hash) ? label : {}
|
|
56
|
+
@path = path.is_a?(String) ? path : nil
|
|
57
|
+
@path_opts = path.is_a?(Hash) ? path : {}
|
|
58
|
+
@method = method&.to_sym
|
|
59
|
+
@feasibility_target = feasibility_target
|
|
60
|
+
@feasibility_action = feasibility_action
|
|
61
|
+
@custom_args = custom_args
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns true for things like User.first, but false for things like :users or User
|
|
65
|
+
def model?
|
|
66
|
+
@model = @data.respond_to?(:model_name) && !@data.is_a?(Class) if @model.nil?
|
|
67
|
+
return @model
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Instanciates the component and returns the instance. If `data` and/or `data_class` were specified when instantiating this intent, they are passed.
|
|
71
|
+
# All given arguments will be given to the component's initializer, also overriding `data` and `data_class` if present.
|
|
72
|
+
def comp(*, **)
|
|
73
|
+
return @comp ||= @comp_class.new(*, data: @data, data_class: @data_class, **)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns the path to the component.
|
|
77
|
+
# Additional arguments are passed to the component's path block, which typically passes them to the Rails path helper.
|
|
78
|
+
# @param model [ApplicationRecord] If given and non-nil, will override the model passed to the component's path block
|
|
79
|
+
# @param standalone_name [Symbol] If given and non-nil, will override the `standalone_name` passed to the component's path block
|
|
80
|
+
def path(model = nil, *, standalone_name: nil, **path_opt_overrides)
|
|
81
|
+
path_opts = @path_opts.deep_merge(path_opt_overrides)
|
|
82
|
+
comp.path(model || (model? ? @data : nil), standalone_name: standalone_name || @standalone_name, **path_opts)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns a name for this intent, consisting of comp and family name. Can be overriden in the constructor.
|
|
86
|
+
# Example: :show_users, :destroy_sessions
|
|
87
|
+
def name
|
|
88
|
+
@name.presence || :"#{comp_class.comp_name}_#{comp_class.family_name}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns the label of buttons produced by this intent.
|
|
92
|
+
def label(model = nil, *, **label_opt_overrides)
|
|
93
|
+
label_opts = @label_opts.deep_merge(label_opt_overrides)
|
|
94
|
+
@label.presence || comp.label(model || (model? ? @data : nil), *, **label_opts)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def method
|
|
98
|
+
@method || :get
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def feasibility_target
|
|
102
|
+
@feasibility_target.presence || model? ? @data : nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def feasibility_action
|
|
106
|
+
@feasibility_action.presence || comp_class.comp_name.to_sym
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns whether this intent is feasible (no prevention)
|
|
110
|
+
def feasible?
|
|
111
|
+
return true if feasibility_target.blank? || feasibility_action.blank?
|
|
112
|
+
return feasibility_target.feasible?(feasibility_action)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Returns the options that are given to the initializer when creating a button from this intent.
|
|
116
|
+
def button_comp_opts(label: {})
|
|
117
|
+
return @custom_args.deep_merge({
|
|
118
|
+
label: label(**label),
|
|
119
|
+
href: feasible? ? path : nil,
|
|
120
|
+
method:,
|
|
121
|
+
class: feasible? ? nil : 'disabled',
|
|
122
|
+
title: feasible? ? nil : feasibility_target.full_feasibility_messages(feasibility_action).presence
|
|
123
|
+
})
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Renders this intent into a button defined by `style`.
|
|
127
|
+
# @param controller [ApplicationController] The controller from the request context, needed to render the button.
|
|
128
|
+
# @param parent_comp [Compony::Component] If called from within a component, pass the component to inform the button that it is nested within.
|
|
129
|
+
# @param style [Symbol] If present, overrides the class of the generated button component, defaults to {Compony#default_button_style}.
|
|
130
|
+
# @param button_comp_opts_overrides [Hash] Any further kwargs are passed to the button component's initializer.
|
|
131
|
+
def render(controller, parent_comp = nil, style: nil, label: {}, **button_comp_opts_overrides)
|
|
132
|
+
# Check if permitted
|
|
133
|
+
return nil unless comp.standalone_access_permitted_for?(controller, standalone_name: @standalone_name, verb: method)
|
|
134
|
+
# Prepare opts
|
|
135
|
+
button_comp_class ||= Compony.button_component_class(*[style].compact)
|
|
136
|
+
button_opts = button_comp_opts(label:).merge(button_comp_opts_overrides)
|
|
137
|
+
# Perform render
|
|
138
|
+
if parent_comp
|
|
139
|
+
return parent_comp.sub_comp(button_comp_class, **button_opts).render(controller)
|
|
140
|
+
else
|
|
141
|
+
button_comp_class.new(**button_opts).render(controller)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -12,7 +12,7 @@ module Compony
|
|
|
12
12
|
return transform_and_join(data.send(@name), controller:) do |el|
|
|
13
13
|
next nil if el.nil?
|
|
14
14
|
if Compony.comp_class_for(link_to_component, el)
|
|
15
|
-
next controller.helpers.
|
|
15
|
+
next controller.helpers.render_intent(link_to_component, el, **{ button: { style: :link } }.deep_merge(link_opts))
|
|
16
16
|
else
|
|
17
17
|
next el.label
|
|
18
18
|
end
|
|
@@ -42,7 +42,12 @@ module Compony
|
|
|
42
42
|
return super
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
################################################
|
|
46
|
+
# View helpers available only inside components:
|
|
47
|
+
################################################
|
|
48
|
+
|
|
45
49
|
# Renders a content block from the current component.
|
|
50
|
+
# Does nothing if no such content block exists.
|
|
46
51
|
def content(name) # rubocop:disable Naming/PredicateMethod
|
|
47
52
|
name = name.to_sym
|
|
48
53
|
content_block = component.content_blocks.find { |el| el.name == name }
|
|
@@ -59,8 +64,24 @@ module Compony
|
|
|
59
64
|
return true
|
|
60
65
|
end
|
|
61
66
|
|
|
67
|
+
# Like {Compony::RequestContext#content}, but fails if no such content block exists.
|
|
68
|
+
# @see Compony::RequestContext#content
|
|
62
69
|
def content!(name)
|
|
63
70
|
content(name) || fail("Content block #{name.inspect} not found in #{component.inspect}.")
|
|
64
71
|
end
|
|
72
|
+
|
|
73
|
+
# View helper that renders an intent through the default button class.
|
|
74
|
+
# All non-mentioned parameters are given to the intent initializer.
|
|
75
|
+
# When inside a request context (`content do...`), this precedes {ViewHelpers#render_intent}.
|
|
76
|
+
# @param button [Hash] Parameters that will be given to the button component initializer.
|
|
77
|
+
def render_intent(*, button: {}, **)
|
|
78
|
+
Compony.intent(*, **).render(controller, component, **button)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# View helper that instanciates a sub comp and renders it.
|
|
82
|
+
# Example usage: `concat render_sub_comp(Components::Something::Nested)`
|
|
83
|
+
def render_sub_comp(...)
|
|
84
|
+
sub_comp(...).render(controller)
|
|
85
|
+
end
|
|
65
86
|
end
|
|
66
87
|
end
|