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,59 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module Components
|
|
3
|
+
# @api description
|
|
4
|
+
# This is the default button implementation, providing a minimal button
|
|
5
|
+
class Button < Compony::Component
|
|
6
|
+
SUPPORTED_TYPES = %i[button submit].freeze
|
|
7
|
+
|
|
8
|
+
# path: If given a block, it will be evaluated in the helpers context when rendering
|
|
9
|
+
# enabled: If given a block, it will be evaluated in the helpers context when rendering
|
|
10
|
+
def initialize(*args, label: nil, path: nil, method: nil, type: nil, enabled: nil, visible: nil, title: nil, **kwargs, &block)
|
|
11
|
+
@label = label || Compony.button_defaults[:label]
|
|
12
|
+
@type = type&.to_sym || Compony.button_defaults[:type] || :button
|
|
13
|
+
@path = path || Compony.button_defaults[:path] || 'javascript:void(0)'
|
|
14
|
+
@method = method || Compony.button_defaults[:method]
|
|
15
|
+
if @type != :button && !@method.nil?
|
|
16
|
+
fail("Param `method` is only allowed for :button type buttons, but got method #{@method.inspect} for type #{@type.inspect}")
|
|
17
|
+
end
|
|
18
|
+
@method ||= :get
|
|
19
|
+
@enabled = enabled
|
|
20
|
+
@enabled = Compony.button_defaults[:enabled] if @enabled.nil?
|
|
21
|
+
@enabled = true if @enabled.nil?
|
|
22
|
+
@visible = visible
|
|
23
|
+
@visible = Compony.button_defaults[:visible] if @visible.nil?
|
|
24
|
+
@visible = true if @visible.nil?
|
|
25
|
+
@title = title || Compony.button_defaults[:title]
|
|
26
|
+
|
|
27
|
+
fail "Unsupported button type #{@type}, use on of: #{SUPPORTED_TYPES.inspect}" unless SUPPORTED_TYPES.include?(@type)
|
|
28
|
+
|
|
29
|
+
super(*args, **kwargs, &block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
setup do
|
|
33
|
+
before_render do
|
|
34
|
+
if @path.respond_to?(:call)
|
|
35
|
+
@path = instance_exec(&@path)
|
|
36
|
+
end
|
|
37
|
+
if @enabled.respond_to?(:call)
|
|
38
|
+
@enabled = @enabled.call(controller)
|
|
39
|
+
end
|
|
40
|
+
if @visible.respond_to?(:call)
|
|
41
|
+
@visible = @visible.call(controller)
|
|
42
|
+
end
|
|
43
|
+
@path = 'javascript:void(0)' unless @enabled
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
content do
|
|
47
|
+
if @visible
|
|
48
|
+
case @type
|
|
49
|
+
when :button
|
|
50
|
+
concat button_to(@label, @path, method: @method, disabled: !@enabled, title: @title)
|
|
51
|
+
when :submit
|
|
52
|
+
concat button_tag(@label, type: :submit, disabled: !@enabled, title: @title)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module Components
|
|
3
|
+
# @api description
|
|
4
|
+
# This component is used for the _form partial in the Rails paradigm.
|
|
5
|
+
class Form < Component
|
|
6
|
+
def initialize(...)
|
|
7
|
+
@schema_lines_for_data = [] # Array of procs taking data returning a Schemacop proc
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def check_config!
|
|
12
|
+
super
|
|
13
|
+
fail "#{inspect} requires config.form_fields do ..." if @form_fields.blank?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
setup do
|
|
17
|
+
before_render do
|
|
18
|
+
# Must render the buttons now as the rendering within simple form breaks the form
|
|
19
|
+
@submit_button = Compony.button_component_class.new(
|
|
20
|
+
label: @submit_label || I18n.t('compony.components.form.submit'), icon: 'arrow-right', type: :submit
|
|
21
|
+
).render(controller)
|
|
22
|
+
@submit_path = @comp_opts[:submit_path]
|
|
23
|
+
@submit_path = @submit_path.call(controller) if @submit_path.respond_to?(:call)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
content do
|
|
27
|
+
form_html = simple_form_for(data, method: @comp_opts[:submit_verb], url: @submit_path) do |f|
|
|
28
|
+
component.with_simpleform(f) do
|
|
29
|
+
instance_exec(&form_fields)
|
|
30
|
+
div @submit_button, class: 'compony-form-buttons'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
concat form_html
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# DSL method, use to set the form content
|
|
38
|
+
def form_fields(&block)
|
|
39
|
+
return @form_fields unless block_given?
|
|
40
|
+
@form_fields = block
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Attr reader for @schema_wrapper_key with auto-calculated default
|
|
44
|
+
def schema_wrapper_key_for(data)
|
|
45
|
+
if @schema_wrapper_key.present?
|
|
46
|
+
return @schema_wrapper_key
|
|
47
|
+
else
|
|
48
|
+
# If schema was not called, auto-infer a default
|
|
49
|
+
data.model_name.singular
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Attr reader for @schema_block with auto-calculated default
|
|
54
|
+
def schema_block_for(data)
|
|
55
|
+
if @schema_block
|
|
56
|
+
return @schema_block
|
|
57
|
+
else
|
|
58
|
+
# If schema was not called, auto-infer a default
|
|
59
|
+
local_schema_lines_for_data = @schema_lines_for_data
|
|
60
|
+
return proc do
|
|
61
|
+
local_schema_lines_for_data.each do |schema_line|
|
|
62
|
+
instance_exec(&schema_line.call(data))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# This method is used by render to store the simpleform instance inside the component such that we can call
|
|
69
|
+
# methods from inside `form_fields`. This is a workaround required because the form does not exist when the
|
|
70
|
+
# RequestContext is being built, and we want the method `field` to be available inside the `form_fields` block.
|
|
71
|
+
# @todo Refactor? Could this be greatly simplified by having `form_field to |f|` ?
|
|
72
|
+
def with_simpleform(simpleform)
|
|
73
|
+
@simpleform = simpleform
|
|
74
|
+
yield
|
|
75
|
+
@simpleform = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Called inside the form_fields block. This makes the method `field` available in the block.
|
|
79
|
+
# See also notes for `with_simpleform`.
|
|
80
|
+
def field(name, **input_opts)
|
|
81
|
+
fail("The `field` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform
|
|
82
|
+
|
|
83
|
+
hidden = input_opts.delete(:hidden)
|
|
84
|
+
model_field = @simpleform.object.fields[name.to_sym]
|
|
85
|
+
fail("Field #{name.to_sym.inspect} is not defined on #{@simpleform.object.inspect} but was requested in #{inspect}.") unless model_field
|
|
86
|
+
|
|
87
|
+
if hidden
|
|
88
|
+
return @simpleform.input model_field.schema_key, as: :hidden, **input_opts
|
|
89
|
+
else
|
|
90
|
+
return model_field.simpleform_input(@simpleform, self, **input_opts)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Called inside the form_fields block. This makes the method `f` available in the block.
|
|
95
|
+
# See also notes for `with_simpleform`.
|
|
96
|
+
def f
|
|
97
|
+
fail("The `f` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform
|
|
98
|
+
return @simpleform
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Quick access for wrapping collections in Rails compatible format
|
|
102
|
+
def collect(...)
|
|
103
|
+
Compony::ModelFields::Anchormodel.collect(...)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
protected
|
|
107
|
+
|
|
108
|
+
# DSL method, adds a new line to the schema whitelisting a single param inside the schema's wrapper
|
|
109
|
+
def schema_line(&block)
|
|
110
|
+
@schema_lines_for_data << proc { block }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# DSL method, adds a new field to the schema whitelisting a single field of data_class
|
|
114
|
+
# This auto-generates the correct schema line for the field.
|
|
115
|
+
def schema_field(field_name)
|
|
116
|
+
@schema_lines_for_data << proc do |data|
|
|
117
|
+
field = data.class.fields[field_name.to_sym] || fail("No field #{field_name.to_sym.inspect} found for #{data.inspect} in #{inspect}.")
|
|
118
|
+
next field.schema_line
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# DSL method, mass-assigns schema fields
|
|
123
|
+
def schema_fields(*field_names)
|
|
124
|
+
field_names.each { |field_name| schema_field(field_name) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# DSL method, use to replace the form's schema and wrapper key for a completely manual schema
|
|
128
|
+
def schema(wrapper_key, &block)
|
|
129
|
+
if block_given?
|
|
130
|
+
@schema_wrapper_key = wrapper_key
|
|
131
|
+
@schema_block = block
|
|
132
|
+
else
|
|
133
|
+
fail 'schema requires a block to be given'
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module Components
|
|
3
|
+
module Resourceful
|
|
4
|
+
# @api description
|
|
5
|
+
# This component is used for the Rails destroy paradigm. Asks for confirm when queried using GET.
|
|
6
|
+
class Destroy < Compony::Component
|
|
7
|
+
include Compony::ComponentMixins::Resourceful
|
|
8
|
+
|
|
9
|
+
setup do
|
|
10
|
+
standalone path: "#{family_name}/:id/destroy" do
|
|
11
|
+
verb :get do
|
|
12
|
+
authorize { can?(:destroy, @data) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
verb :delete do
|
|
16
|
+
authorize { can?(:destroy, @data) }
|
|
17
|
+
store_data # This enables the global store_data block defined below for this path and verb.
|
|
18
|
+
respond do
|
|
19
|
+
evaluate_with_backfire(&@on_destroyed_block)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
label(:long) { |data| I18n.t('compony.components.destroy.label.long', data_label: data.label) }
|
|
25
|
+
label(:short) { |_| I18n.t('compony.components.destroy.label.short') }
|
|
26
|
+
icon { :trash }
|
|
27
|
+
color { :danger }
|
|
28
|
+
|
|
29
|
+
content do
|
|
30
|
+
div I18n.t('compony.components.destroy.confirm_question', data_label: @data.label)
|
|
31
|
+
div do
|
|
32
|
+
concat compony_button(comp_cst,
|
|
33
|
+
@data,
|
|
34
|
+
label: I18n.t('compony.components.destroy.confirm_button'),
|
|
35
|
+
method: :delete)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
store_data do
|
|
40
|
+
# Validate params against the form's schema
|
|
41
|
+
local_data = @data # Capture data for usage in the Schemacop call
|
|
42
|
+
schema = Schemacop::Schema3.new :hash, additional_properties: true do
|
|
43
|
+
if local_data.class.primary_key_type_key == :string
|
|
44
|
+
str! :id
|
|
45
|
+
else
|
|
46
|
+
int! :id, cast_str: true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
schema.validate!(controller.request.params)
|
|
50
|
+
|
|
51
|
+
# Perform destroy
|
|
52
|
+
@data.destroy!
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
on_destroyed do
|
|
56
|
+
flash.notice = I18n.t('compony.components.destroy.data_was_destroyed', data_label: @data.label)
|
|
57
|
+
redirect_to evaluate_with_backfire(&@on_destroyed_redirect_path_block), status: :see_other # 303: force GET
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
on_destroyed_redirect_path do
|
|
61
|
+
Compony.path(:index, family_cst)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# DSL method
|
|
66
|
+
def on_destroyed(&block)
|
|
67
|
+
@on_destroyed_block = block
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# DSL method
|
|
71
|
+
def on_destroyed_redirect_path(&block)
|
|
72
|
+
@on_destroyed_redirect_path_block = block
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module Components
|
|
3
|
+
module Resourceful
|
|
4
|
+
# @api description
|
|
5
|
+
# This component is used for the Rails edit and update paradigm. Performs update when the form is submitted.
|
|
6
|
+
class Edit < Compony::Components::WithForm
|
|
7
|
+
include Compony::ComponentMixins::Resourceful
|
|
8
|
+
setup do
|
|
9
|
+
submit_verb :patch
|
|
10
|
+
standalone path: "#{family_name}/:id/edit" do
|
|
11
|
+
verb :get do
|
|
12
|
+
authorize { can?(:edit, @data) }
|
|
13
|
+
assign_attributes # This enables the global assign_attributes block defined below for this path and verb.
|
|
14
|
+
end
|
|
15
|
+
verb submit_verb do
|
|
16
|
+
authorize { can?(:update, @data) }
|
|
17
|
+
assign_attributes # This enables the global assign_attributes block defined below for this path and verb.
|
|
18
|
+
store_data # This enables the global store_data block defined below for this path and verb.
|
|
19
|
+
respond do
|
|
20
|
+
if @update_succeeded
|
|
21
|
+
evaluate_with_backfire(&@on_updated_block)
|
|
22
|
+
else
|
|
23
|
+
evaluate_with_backfire(&@on_update_failed_block)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
label(:long) { |data| I18n.t('compony.components.edit.label.long', data_label: data.label) }
|
|
30
|
+
label(:short) { |_| I18n.t('compony.components.edit.label.short') }
|
|
31
|
+
icon { :pencil }
|
|
32
|
+
|
|
33
|
+
content do
|
|
34
|
+
concat form_comp.render(controller, data: @data)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
assign_attributes do
|
|
38
|
+
# Validate params against the form's schema
|
|
39
|
+
local_form_comp = form_comp # Capture form_comp for usage in the Schemacop call
|
|
40
|
+
local_data = @data # Capture data for usage in the Schemacop call
|
|
41
|
+
schema = Schemacop::Schema3.new :hash, additional_properties: true do
|
|
42
|
+
if local_data.class.primary_key_type_key == :string
|
|
43
|
+
str! :id
|
|
44
|
+
else
|
|
45
|
+
int! :id, cast_str: true
|
|
46
|
+
end
|
|
47
|
+
hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data)
|
|
48
|
+
end
|
|
49
|
+
schema.validate!(controller.request.params)
|
|
50
|
+
|
|
51
|
+
# TODO: Why are we not saving the validated params?
|
|
52
|
+
attrs_to_assign = controller.request.params[form_comp.schema_wrapper_key_for(@data)]
|
|
53
|
+
@data.assign_attributes(attrs_to_assign) if attrs_to_assign
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
store_data do
|
|
57
|
+
@update_succeeded = @data.save
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
on_updated do
|
|
61
|
+
flash.notice = I18n.t('compony.components.edit.data_was_updated', data_label: data.label)
|
|
62
|
+
redirect_to evaluate_with_backfire(&@on_updated_redirect_path_block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
on_updated_redirect_path do
|
|
66
|
+
if Compony.comp_class_for(:show, @data)
|
|
67
|
+
Compony.path(:show, @data)
|
|
68
|
+
else
|
|
69
|
+
Compony.path(:index, @data)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
on_update_failed do
|
|
74
|
+
Rails.logger.warn(@data&.errors&.full_messages)
|
|
75
|
+
render_standalone(controller, status: :unprocessable_entity)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# DSL method
|
|
80
|
+
def on_updated(&block)
|
|
81
|
+
@on_updated_block = block
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# DSL method
|
|
85
|
+
def on_updated_redirect_path(&block)
|
|
86
|
+
@on_updated_redirect_path_block = block
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# DSL method
|
|
90
|
+
def on_update_failed(&block)
|
|
91
|
+
@on_update_failed_block = block
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module Components
|
|
3
|
+
module Resourceful
|
|
4
|
+
# @api description
|
|
5
|
+
# This component is used for the Rails new and create paradigm. Performs update when the form is submitted.
|
|
6
|
+
class New < Compony::Components::WithForm
|
|
7
|
+
include Compony::ComponentMixins::Resourceful
|
|
8
|
+
|
|
9
|
+
setup do
|
|
10
|
+
submit_verb :post
|
|
11
|
+
load_data { @data = data_class.new }
|
|
12
|
+
standalone path: "#{family_name}/new" do
|
|
13
|
+
verb :get do
|
|
14
|
+
authorize { can?(:create, data_class) }
|
|
15
|
+
assign_attributes # This enables the global assign_attributes block defined below for this path and verb.
|
|
16
|
+
end
|
|
17
|
+
verb submit_verb do
|
|
18
|
+
authorize { can?(:create, data_class) }
|
|
19
|
+
assign_attributes # This enables the global assign_attributes block defined below for this path and verb.
|
|
20
|
+
store_data # This enables the global store_data block defined below for this path and verb.
|
|
21
|
+
respond do
|
|
22
|
+
if @create_succeeded
|
|
23
|
+
evaluate_with_backfire(&@on_created_block)
|
|
24
|
+
else
|
|
25
|
+
evaluate_with_backfire(&@on_create_failed_block)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
label(:long) { I18n.t('compony.components.new.label.long', data_class: data_class.model_name.human) }
|
|
32
|
+
label(:short) { I18n.t('compony.components.new.label.short') }
|
|
33
|
+
icon { :plus }
|
|
34
|
+
|
|
35
|
+
add_content do
|
|
36
|
+
h2 component.label
|
|
37
|
+
end
|
|
38
|
+
add_content do
|
|
39
|
+
concat form_comp.render(controller, data: @data)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
assign_attributes do
|
|
43
|
+
local_form_comp = form_comp # Capture form_comp for usage in the Schemacop call
|
|
44
|
+
local_data = @data # Capture data for usage in the Schemacop call
|
|
45
|
+
schema = Schemacop::Schema3.new :hash, additional_properties: true do
|
|
46
|
+
hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data)
|
|
47
|
+
end
|
|
48
|
+
schema.validate!(controller.request.params)
|
|
49
|
+
|
|
50
|
+
# TODO: Why are we not saving the validated params?
|
|
51
|
+
attrs_to_assign = controller.request.params[form_comp.schema_wrapper_key_for(@data)]
|
|
52
|
+
@data.assign_attributes(attrs_to_assign) if attrs_to_assign
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
store_data do
|
|
56
|
+
@create_succeeded = @data.save
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
on_created do
|
|
60
|
+
flash.notice = I18n.t('compony.components.new.data_was_created', data_label: data.label)
|
|
61
|
+
redirect_to evaluate_with_backfire(&@on_created_redirect_path_block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
on_created_redirect_path do
|
|
65
|
+
if Compony.comp_class_for(:show, @data)
|
|
66
|
+
Compony.path(:show, @data)
|
|
67
|
+
else
|
|
68
|
+
Compony.path(:index, @data)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
on_create_failed do
|
|
73
|
+
Rails.logger.warn(@data&.errors&.full_messages)
|
|
74
|
+
render_standalone(controller, status: :unprocessable_entity)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# DSL method
|
|
79
|
+
def on_created(&block)
|
|
80
|
+
@on_created_block = block
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# DSL method
|
|
84
|
+
def on_created_redirect_path(&block)
|
|
85
|
+
@on_created_redirect_path_block = block
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# DSL method
|
|
89
|
+
def on_create_failed(&block)
|
|
90
|
+
@on_create_failed_block = block
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module Components
|
|
3
|
+
# @api description
|
|
4
|
+
# This component is destined to take a sub-component that is a form component.
|
|
5
|
+
# It can be called via :get or via `submit_verb` depending on whether its form should be shown or submitted.
|
|
6
|
+
class WithForm < Component
|
|
7
|
+
# Returns an instance of the form component responsible for rendering the form.
|
|
8
|
+
# Feel free to override this in subclasses.
|
|
9
|
+
def form_comp
|
|
10
|
+
@form_comp ||= (form_comp_class || comp_class_for!(:form, family_cst)).new(
|
|
11
|
+
self,
|
|
12
|
+
submit_verb:,
|
|
13
|
+
# 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.
|
|
14
|
+
submit_path: ->(controller) { controller.helpers.send("#{Compony.path_helper_name(comp_name, family_name)}_path") }
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# DSL method
|
|
19
|
+
# Sets or returns the previously set submit verb
|
|
20
|
+
def submit_verb(new_submit_verb = nil)
|
|
21
|
+
if new_submit_verb.present?
|
|
22
|
+
new_submit_verb = new_submit_verb.to_sym
|
|
23
|
+
available_verbs = ComponentMixins::Default::Standalone::VerbDsl::AVAILABLE_VERBS
|
|
24
|
+
fail "Unknown HTTP verb #{new_submit_verb.inspect}, use one of #{available_verbs.inspect}" unless available_verbs.include?(new_submit_verb)
|
|
25
|
+
@submit_verb = new_submit_verb
|
|
26
|
+
end
|
|
27
|
+
return @submit_verb || fail("WithForm component #{self} is missing a call to `submit_verb`.")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# DSL method
|
|
31
|
+
# Overrides the form comp class that is instanciated to render the form
|
|
32
|
+
def form_comp_class(new_form_comp_class = nil)
|
|
33
|
+
@form_comp_class ||= new_form_comp_class
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module ControllerMixin
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
include Compony::ViewHelpers
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# Declare all methods in each such module as helper_method
|
|
9
|
+
Compony::ViewHelpers.public_instance_methods.each { |helper_method_sym| helper_method helper_method_sym }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
class Engine < Rails::Engine
|
|
3
|
+
initializer 'compony.configure_eager_load_paths', before: :load_environment_hook, group: :all do
|
|
4
|
+
# Allow app/components/foo/bar.rb to define constants Components::Foo::Bar and make sure components are eager loaded (needed for route generation etc.)
|
|
5
|
+
Rails.application.config.eager_load_paths.delete(Rails.root.join('app', 'components').to_s)
|
|
6
|
+
Rails.application.config.eager_load_paths.unshift(Rails.root.join('app').to_s)
|
|
7
|
+
|
|
8
|
+
# Prevent *.rb files in assets and views directories to be loaded
|
|
9
|
+
Rails.autoloaders.main.ignore(Rails.root.join('app', 'assets').to_s)
|
|
10
|
+
Rails.autoloaders.main.ignore(Rails.root.join('app', 'views').to_s)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer 'compony.controller_mixin' do
|
|
14
|
+
ActiveSupport.on_load :action_controller_base do
|
|
15
|
+
include Compony::ControllerMixin
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
# @api description
|
|
3
|
+
# This class is intended for configs with predefined interfaces and should be used with instances of Hash:<br>
|
|
4
|
+
# Example:<br>
|
|
5
|
+
# ```ruby
|
|
6
|
+
# instance_of_a_hash = Compony::MethodAccessibleHash.new
|
|
7
|
+
# instance_of_a_hash.merge!({ foo: :bar })
|
|
8
|
+
# instance_of_a_hash.foo --> :bar
|
|
9
|
+
# instance_of_a_hash.roo --> nil
|
|
10
|
+
# ```
|
|
11
|
+
# See: https://gist.github.com/kalsan/87826048ea0ade92ab1be93c0919b405
|
|
12
|
+
class MethodAccessibleHash < Hash
|
|
13
|
+
# Takes an optional hash as argument and constructs a new
|
|
14
|
+
# MethodAccessibleHash.
|
|
15
|
+
def initialize(hash = {})
|
|
16
|
+
super()
|
|
17
|
+
|
|
18
|
+
hash.each do |key, value|
|
|
19
|
+
self[key.to_sym] = value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @private
|
|
24
|
+
def merge(hash)
|
|
25
|
+
super(hash.symbolize_keys)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @private
|
|
29
|
+
def method_missing(method, *args, &)
|
|
30
|
+
if method.end_with?('=')
|
|
31
|
+
name = method.to_s.gsub(/=$/, '')
|
|
32
|
+
self[name.to_sym] = args.first
|
|
33
|
+
else
|
|
34
|
+
self[method.to_sym]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @private
|
|
39
|
+
def respond_to_missing?(_method, _include_private = false)
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module ModelFields
|
|
3
|
+
class Anchormodel < Base
|
|
4
|
+
# Takes an array of objects implementing the methods `label` and `key` and returns an array suitable for simple_form select fields.
|
|
5
|
+
def self.collect(flat_array, label_method: :label, key_method: :key)
|
|
6
|
+
return flat_array.map { |entry| [entry.send(label_method), entry.send(key_method)] }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def value_for(data, controller: nil, **_)
|
|
10
|
+
return transform_and_join(data.send(@name), controller:, &:label)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def simpleform_input(form, _component, **input_opts)
|
|
14
|
+
selected_cst = form.object.send(@name)
|
|
15
|
+
anchormodel_attribute = @model_class.anchormodel_attributes[@name]
|
|
16
|
+
anchormodel_class = anchormodel_attribute.anchormodel_class
|
|
17
|
+
opts = {
|
|
18
|
+
collection: self.class.collect(anchormodel_class.all),
|
|
19
|
+
label_method: :first,
|
|
20
|
+
value_method: :second,
|
|
21
|
+
selected: selected_cst&.key || anchormodel_class.all.first,
|
|
22
|
+
include_blank: anchormodel_attribute.optional
|
|
23
|
+
}.merge(input_opts)
|
|
24
|
+
return form.input @name, **opts
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Compony
|
|
2
|
+
module ModelFields
|
|
3
|
+
class Association < Base
|
|
4
|
+
def initialize(...)
|
|
5
|
+
super
|
|
6
|
+
resolve_association!
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def value_for(data, link_to_component: nil, link_opts: {}, controller: nil)
|
|
10
|
+
if link_to_component
|
|
11
|
+
return transform_and_join(data.send(@name), controller:) do |el|
|
|
12
|
+
el.nil? ? nil : controller.helpers.compony_link(link_to_component, el, **link_opts)
|
|
13
|
+
end
|
|
14
|
+
else
|
|
15
|
+
return transform_and_join(data.send(@name), controller:) { |el| el&.label }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def schema_line
|
|
20
|
+
local_schema_key = @schema_key # Capture schema_key as it will not be available within the lambda
|
|
21
|
+
if multi?
|
|
22
|
+
return proc do
|
|
23
|
+
ary? local_schema_key do
|
|
24
|
+
list :integer, cast_str: true
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
return proc do
|
|
29
|
+
int? local_schema_key, cast_str: true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def simpleform_input(form, _component, **input_opts)
|
|
35
|
+
return form.association @name, **input_opts
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
# Uses Rails methods to figure out the arity, schema key etc. and store them.
|
|
41
|
+
# This can be auto-inferred without accessing the database.
|
|
42
|
+
def resolve_association!
|
|
43
|
+
@association = true
|
|
44
|
+
association_info = @model_class.reflect_on_association(@name) || fail("Association #{@name.inspect} does not exist for #{@model_class.inspect}.")
|
|
45
|
+
@multi = association_info.macro == :has_many
|
|
46
|
+
id_name = "#{@name.to_s.singularize}_id"
|
|
47
|
+
@schema_key = @multi ? id_name.pluralize.to_sym : id_name.to_sym
|
|
48
|
+
rescue ActiveRecord::NoDatabaseError
|
|
49
|
+
Rails.logger.warn('Warning: Compony could not auto-detect fields due to missing database. This is ok when running db:create.')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|