super 0.0.5 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +4 -1
  3. data/CONTRIBUTING.md +56 -0
  4. data/README.md +68 -38
  5. data/STABILITY.md +50 -0
  6. data/app/assets/javascripts/super/application.js +1170 -359
  7. data/app/assets/stylesheets/super/application.css +78105 -50441
  8. data/app/controllers/super/application_controller.rb +44 -52
  9. data/app/helpers/super/form_builder_helper.rb +23 -0
  10. data/app/views/layouts/super/application.html.erb +26 -6
  11. data/app/views/super/application/{_resources_header.html.erb → _collection_header.html.erb} +5 -6
  12. data/app/views/super/application/_display_rich_text.html.erb +1 -0
  13. data/app/views/super/application/_filter.html.erb +14 -0
  14. data/app/views/super/application/_filter_type_select.html.erb +18 -0
  15. data/app/views/super/application/_filter_type_text.html.erb +16 -0
  16. data/app/views/super/application/_filter_type_timestamp.html.erb +23 -0
  17. data/app/views/super/application/_flash.html.erb +13 -13
  18. data/app/views/super/application/_form_field__destroy.html.erb +1 -9
  19. data/app/views/super/application/_form_field_checkbox.html.erb +1 -0
  20. data/app/views/super/application/_form_field_rich_text_area.html.erb +1 -0
  21. data/app/views/super/application/_form_field_select.html.erb +1 -23
  22. data/app/views/super/application/_form_field_text.html.erb +1 -13
  23. data/app/views/super/application/_form_has_many.html.erb +1 -1
  24. data/app/views/super/application/{_resource_header.html.erb → _member_header.html.erb} +6 -6
  25. data/app/views/super/application/_super_layout.html.erb +12 -17
  26. data/app/views/super/application/_super_pagination.html.erb +16 -0
  27. data/app/views/super/application/_super_panel.html.erb +3 -7
  28. data/app/views/super/application/_super_schema_display_actions.html.erb +5 -0
  29. data/app/views/super/application/_super_schema_display_index.html.erb +24 -0
  30. data/app/views/super/application/_super_schema_display_show.html.erb +8 -0
  31. data/app/views/super/application/_super_schema_form.html.erb +15 -0
  32. data/app/views/super/application/edit.html.erb +2 -6
  33. data/app/views/super/application/index.html.erb +2 -6
  34. data/app/views/super/application/new.html.erb +2 -6
  35. data/app/views/super/application/show.html.erb +2 -6
  36. data/app/views/super/feather/{_chevron_down.svg → _chevron_down.html} +0 -0
  37. data/config/locales/en.yml +5 -0
  38. data/docs/README.md +4 -2
  39. data/docs/action_text.md +48 -0
  40. data/docs/cheat.md +41 -0
  41. data/docs/faq.md +3 -3
  42. data/docs/installation.md +21 -0
  43. data/docs/quick_start.md +1 -16
  44. data/docs/webpacker.md +13 -5
  45. data/frontend/super-frontend/dist/application.css +78105 -50441
  46. data/frontend/super-frontend/dist/application.js +1170 -359
  47. data/lib/generators/super/action_text/USAGE +23 -0
  48. data/lib/generators/super/action_text/action_text_generator.rb +30 -0
  49. data/lib/generators/super/action_text/templates/pack_super_action_text.css +23 -0
  50. data/lib/generators/super/action_text/templates/pack_super_action_text.js +4 -0
  51. data/lib/generators/super/install/install_generator.rb +16 -0
  52. data/lib/generators/super/resource/templates/resources_controller.rb.tt +1 -31
  53. data/lib/generators/super/webpacker/USAGE +5 -4
  54. data/lib/generators/super/webpacker/webpacker_generator.rb +3 -2
  55. data/lib/super.rb +21 -3
  56. data/lib/super/action_inquirer.rb +2 -2
  57. data/lib/super/assets.rb +112 -38
  58. data/lib/super/client_error.rb +43 -0
  59. data/lib/super/compatibility.rb +13 -1
  60. data/lib/super/configuration.rb +21 -69
  61. data/lib/super/controls.rb +9 -116
  62. data/lib/super/controls/optional.rb +79 -0
  63. data/lib/super/controls/required.rb +13 -0
  64. data/lib/super/controls/steps.rb +114 -0
  65. data/lib/super/display.rb +66 -3
  66. data/lib/super/display/guesser.rb +34 -0
  67. data/lib/super/display/schema_types.rb +61 -25
  68. data/lib/super/engine.rb +9 -1
  69. data/lib/super/error.rb +17 -9
  70. data/lib/super/filter.rb +12 -0
  71. data/lib/super/filter/form_object.rb +97 -0
  72. data/lib/super/filter/guesser.rb +30 -0
  73. data/lib/super/filter/operator.rb +103 -0
  74. data/lib/super/filter/plugin.rb +47 -0
  75. data/lib/super/filter/schema_types.rb +112 -0
  76. data/lib/super/form.rb +35 -0
  77. data/lib/super/form/builder.rb +204 -0
  78. data/lib/super/form/guesser.rb +27 -0
  79. data/lib/super/form/inline_errors.rb +26 -0
  80. data/lib/super/form/schema_types.rb +29 -22
  81. data/lib/super/form/strong_params.rb +29 -0
  82. data/lib/super/layout.rb +28 -0
  83. data/lib/super/link.rb +55 -32
  84. data/lib/super/pagination.rb +55 -0
  85. data/lib/super/panel.rb +13 -0
  86. data/lib/super/partial.rb +12 -0
  87. data/lib/super/partial/resolving.rb +24 -0
  88. data/lib/super/plugin.rb +34 -63
  89. data/lib/super/schema.rb +12 -22
  90. data/lib/super/schema/common.rb +25 -0
  91. data/lib/super/schema/guesser.rb +77 -0
  92. data/lib/super/version.rb +1 -1
  93. data/lib/super/view_helper.rb +1 -20
  94. metadata +90 -33
  95. data/app/helpers/super/application_helper.rb +0 -32
  96. data/app/views/super/application/_form.html.erb +0 -17
  97. data/app/views/super/application/_form_inline_errors.html.erb +0 -10
  98. data/app/views/super/application/_index.html.erb +0 -45
  99. data/app/views/super/application/_show.html.erb +0 -10
  100. data/docs/controls.md +0 -39
  101. data/frontend/super-frontend/build.js +0 -36
  102. data/frontend/super-frontend/package.json +0 -21
  103. data/frontend/super-frontend/postcss.config.js +0 -6
  104. data/frontend/super-frontend/src/javascripts/super/application.ts +0 -18
  105. data/frontend/super-frontend/src/javascripts/super/apply_template_controller.ts +0 -21
  106. data/frontend/super-frontend/src/javascripts/super/rails__ujs.d.ts +0 -1
  107. data/frontend/super-frontend/src/javascripts/super/toggle_pending_destruction_controller.ts +0 -15
  108. data/frontend/super-frontend/src/stylesheets/super/application.css +0 -77
  109. data/frontend/super-frontend/tailwind.config.js +0 -9
  110. data/frontend/super-frontend/tsconfig.json +0 -13
  111. data/frontend/super-frontend/yarn.lock +0 -5540
  112. data/lib/super/step.rb +0 -36
  113. data/lib/tasks/super_tasks.rake +0 -4
@@ -0,0 +1,47 @@
1
+ module Super
2
+ class Filter
3
+ module ControllerMethods
4
+ def index
5
+ super
6
+ @filter_form = controls.initialize_filtering(params: params, query_params: request.GET)
7
+ @records = controls.filter_records(filter_form: @filter_form, records: @records)
8
+ @view.asides.push(:@filter_form)
9
+ end
10
+ end
11
+ end
12
+
13
+ class Controls
14
+ module Optional
15
+ def filters_enabled?
16
+ true
17
+ end
18
+
19
+ def filter_schema
20
+ Filter.new do |fields, type|
21
+ Filter::Guesser.new(model: model, fields: fields, type: type).call
22
+ end
23
+ end
24
+ end
25
+
26
+ module Steps
27
+ def initialize_filtering(params:, query_params:)
28
+ if filters_enabled?
29
+ Super::Filter::FormObject.new(
30
+ model: model,
31
+ schema: filter_schema,
32
+ params: params[:q],
33
+ url: query_params
34
+ )
35
+ end
36
+ end
37
+
38
+ def filter_records(filter_form:, records:)
39
+ if filters_enabled? && records
40
+ filter_form.to_search_query(records)
41
+ else
42
+ records
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,112 @@
1
+ module Super
2
+ class Filter
3
+ # This schema type is used to configure the filtering form on your +#index+
4
+ # action.
5
+ #
6
+ # The +operators:+ keyword argument can be left out in each case. There is
7
+ # a default set of operators that are provided.
8
+ #
9
+ # Note: The constants under "Defined Under Namespace" are considered
10
+ # private.
11
+ #
12
+ # class MemberDashboard
13
+ # # ...
14
+ #
15
+ # def filter_schema
16
+ # Super::Filter.new do |fields, type|
17
+ # fields[:name] = type.text(operators: [
18
+ # Super::Filter::Operator.eq,
19
+ # Super::Filter::Operator.contain,
20
+ # Super::Filter::Operator.ncontain,
21
+ # Super::Filter::Operator.start,
22
+ # Super::Filter::Operator.end,
23
+ # ])
24
+ # fields[:rank] = type.select(collection: Member.ranks.values)
25
+ # fields[:position] = type.text(operators: [
26
+ # Super::Filter::Operator.eq,
27
+ # Super::Filter::Operator.neq,
28
+ # Super::Filter::Operator.contain,
29
+ # Super::Filter::Operator.ncontain,
30
+ # ])
31
+ # fields[:ship_id] = type.select(
32
+ # collection: Ship.all.map { |s| ["#{s.name} (Ship ##{s.id})", s.id] },
33
+ # )
34
+ # fields[:created_at] = type.timestamp
35
+ # fields[:updated_at] = type.timestamp
36
+ # end
37
+ # end
38
+ #
39
+ # # ...
40
+ # end
41
+ class SchemaTypes
42
+ class Text
43
+ def initialize(partial_path:, operators:)
44
+ @partial_path = partial_path
45
+ @operators = operators
46
+ end
47
+
48
+ attr_reader :operators
49
+
50
+ def to_partial_path
51
+ @partial_path
52
+ end
53
+
54
+ def q
55
+ [:q]
56
+ end
57
+ end
58
+
59
+ class Select
60
+ def initialize(collection:, operators:)
61
+ @collection = collection
62
+ @operators = operators
63
+ end
64
+
65
+ attr_reader :collection
66
+ attr_reader :operators
67
+
68
+ def to_partial_path
69
+ "filter_type_select"
70
+ end
71
+
72
+ def q
73
+ [:q]
74
+ end
75
+ end
76
+
77
+ class Timestamp
78
+ def initialize(operators:)
79
+ @operators = operators
80
+ end
81
+
82
+ attr_reader :operators
83
+
84
+ def to_partial_path
85
+ "filter_type_timestamp"
86
+ end
87
+
88
+ def q
89
+ [:q0, :q1]
90
+ end
91
+ end
92
+
93
+ def select(collection:, operators: Filter::Operator.select_defaults)
94
+ Select.new(
95
+ collection: collection,
96
+ operators: operators
97
+ )
98
+ end
99
+
100
+ def text(operators: Filter::Operator.text_defaults)
101
+ Text.new(
102
+ partial_path: "filter_type_text",
103
+ operators: operators
104
+ )
105
+ end
106
+
107
+ def timestamp(operators: Filter::Operator.range_defaults)
108
+ Timestamp.new(operators: operators)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,35 @@
1
+ module Super
2
+ # This schema type is used on your +#edit+ and +#new+ forms
3
+ #
4
+ # ```ruby
5
+ # class MembersController::Controls
6
+ # # ...
7
+ #
8
+ # def new_schema
9
+ # Super::Form.new do |fields, type|
10
+ # fields[:name] = type.string
11
+ # fields[:rank] = type.select
12
+ # fields[:position] = type.string
13
+ # fields[:ship_id] = type.select(
14
+ # collection: Ship.all.map { |s| ["#{s.name} (Ship ##{s.id})", s.id] },
15
+ # )
16
+ # end
17
+ # end
18
+ #
19
+ # # ...
20
+ # end
21
+ # ```
22
+ class Form
23
+ include Schema::Common
24
+
25
+ def initialize
26
+ @fields = Schema::Fields.new
27
+ @schema_types = SchemaTypes.new(fields: @fields)
28
+ yield(@fields, @schema_types)
29
+ end
30
+
31
+ def to_partial_path
32
+ "super_schema_form"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,204 @@
1
+ module Super
2
+ class Form
3
+ # Example
4
+ #
5
+ # ```ruby
6
+ # super_form_for([:admin, @member]) do |f|
7
+ # # the long way
8
+ # f.super.label :name
9
+ # f.super.text_field :name
10
+ # f.super.inline_errors :name
11
+ #
12
+ # # the short way (slightly different from the long way, for alignment)
13
+ # f.super.text_field! :position
14
+ # end
15
+ # ```
16
+ #
17
+ # Refer to the Rails docs:
18
+ # https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html
19
+ class Builder < ActionView::Helpers::FormBuilder
20
+ FIELD_ERROR_PROC = proc { |html_tag, instance| html_tag }
21
+ FORM_BUILDER_DEFAULTS = { builder: self }.freeze
22
+
23
+ def super(**options)
24
+ @super_wrappers ||= Wrappers.new(self, @template)
25
+ end
26
+
27
+ class Wrappers
28
+ def initialize(builder, template)
29
+ @builder = builder
30
+ @template = template
31
+ end
32
+
33
+ def inline_errors(attribute)
34
+ if @builder.object
35
+ messages = InlineErrors.error_messages(@builder.object, attribute).map do |msg|
36
+ error_content_tag(msg)
37
+ end
38
+
39
+ @template.safe_join(messages)
40
+ else
41
+ error_content_tag(<<~MSG.html_safe)
42
+ This form doesn't have an object, so something is probably wrong.
43
+ Maybe <code>accepts_nested_attributes_for</code> isn't set up?
44
+ MSG
45
+ end
46
+ end
47
+
48
+ def label(attribute, text = nil, options = {}, &block)
49
+ options, defaults = split_defaults(options, class: "block")
50
+ options[:class] = join_classes(defaults[:class], options[:class])
51
+
52
+ @builder.label(attribute, text, options, &block)
53
+ end
54
+
55
+ def check_box(attribute, options = {}, checked_value = "1", unchecked_value = "0")
56
+ @builder.check_box(attribute, options, checked_value, unchecked_value)
57
+ end
58
+
59
+ def password_field(attribute, options = {})
60
+ options, defaults = split_defaults(options, class: "super-input w-full")
61
+ options[:class] = join_classes(defaults[:class], options[:class])
62
+
63
+ @builder.password_field(attribute, options)
64
+ end
65
+
66
+ def rich_text_area(attribute, options = {})
67
+ options, defaults = split_defaults(options, class: "trix-content super-input w-full")
68
+ options[:class] = join_classes(defaults[:class], options[:class])
69
+
70
+ @builder.rich_text_area(attribute, options)
71
+ end
72
+
73
+ def select(attribute, choices, options = {}, html_options = {}, &block)
74
+ options, defaults = split_defaults(options, include_blank: true)
75
+ options = defaults.merge(options)
76
+ html_options, html_defaults = split_defaults(html_options, class: "super-input super-input-select-field")
77
+ html_options[:class] = join_classes(html_defaults[:class], html_options[:class])
78
+
79
+ parts = [
80
+ %(<div class="super-input-select">).html_safe,
81
+ @builder.select(attribute, choices, options, html_options, &block),
82
+ <<~HTML.html_safe,
83
+ <div class="super-input-select-icon text-blue-700">
84
+ <span class="h-4 w-4">
85
+ HTML
86
+ @template.render("super/feather/chevron_down"),
87
+ <<~HTML.html_safe,
88
+ </span>
89
+ </div>
90
+ HTML
91
+ %(</div>).html_safe,
92
+ ]
93
+
94
+ @template.safe_join(parts)
95
+ end
96
+
97
+ def submit(value = nil, options = {})
98
+ value, options = nil, value if value.is_a?(Hash)
99
+ options, defaults = split_defaults(options, class: "super-button")
100
+ options[:class] = join_classes(defaults[:class], options[:class])
101
+
102
+ @builder.submit(value, options)
103
+ end
104
+
105
+ def text_field(attribute, options = {})
106
+ options, defaults = split_defaults(options, class: "super-input w-full")
107
+ options[:class] = join_classes(defaults[:class], options[:class])
108
+
109
+ @builder.text_field(attribute, options)
110
+ end
111
+
112
+ def container(&block)
113
+ @template.content_tag(:div, class: "super-field-group", &block)
114
+ end
115
+
116
+ def check_box!(attribute, checked_value = "1", unchecked_value = "0", label: {}, field: {}, show_errors: true)
117
+ label[:super] ||= {}
118
+ label[:super] = { class: "select-none ml-1" }.merge(label[:super])
119
+ container do
120
+ compact_join([
121
+ "<div>".html_safe,
122
+ public_send(:check_box, attribute, field, checked_value, unchecked_value),
123
+ public_send(:label, attribute, nil, label),
124
+ "</div>".html_safe,
125
+ show_errors && inline_errors(attribute),
126
+ ])
127
+ end
128
+ end
129
+
130
+ def password_field!(attribute, label: {}, field: {}, show_errors: true)
131
+ container do
132
+ compact_join([
133
+ public_send(:label, attribute, label),
134
+ %(<div class="mt-1">).html_safe,
135
+ public_send(:password_field, attribute, field),
136
+ show_errors && inline_errors(attribute),
137
+ %(</div>).html_safe,
138
+ ])
139
+ end
140
+ end
141
+
142
+ def rich_text_area!(attribute, label: {}, field: {}, show_errors: true)
143
+ container do
144
+ compact_join([
145
+ public_send(:label, attribute, label),
146
+ %(<div class="mt-1">).html_safe,
147
+ public_send(:rich_text_area, attribute, field),
148
+ show_errors && inline_errors(attribute),
149
+ %(</div>).html_safe,
150
+ ])
151
+ end
152
+ end
153
+
154
+ def select!(attribute, collection, label: {}, field: {}, show_errors: true)
155
+ container do
156
+ compact_join([
157
+ public_send(:label, attribute, label),
158
+ %(<div class="mt-1">).html_safe,
159
+ public_send(:select, attribute, collection, field),
160
+ show_errors && inline_errors(attribute),
161
+ %(</div>).html_safe,
162
+ ])
163
+ end
164
+ end
165
+
166
+ def text_field!(attribute, label: {}, field: {}, show_errors: true)
167
+ container do
168
+ compact_join([
169
+ public_send(:label, attribute, label),
170
+ %(<div class="mt-1">).html_safe,
171
+ public_send(:text_field, attribute, field),
172
+ show_errors && inline_errors(attribute),
173
+ %(</div>).html_safe,
174
+ ])
175
+ end
176
+ end
177
+
178
+ private
179
+
180
+ def split_defaults(options, **internal_defaults)
181
+ defaults = options.delete(:super) || {}
182
+ # prefer options set in `defaults`, since they are user overrides
183
+ defaults = internal_defaults.merge(defaults)
184
+
185
+ [options, defaults]
186
+ end
187
+
188
+ def join_classes(*class_lists)
189
+ class_lists.flatten.map(&:presence).compact
190
+ end
191
+
192
+ def error_content_tag(content)
193
+ @template.content_tag(:p, content, class: "text-red-400 text-xs italic pt-1")
194
+ end
195
+
196
+ def compact_join(*parts)
197
+ @template.safe_join(
198
+ parts.flatten.map(&:presence).compact
199
+ )
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,27 @@
1
+ module Super
2
+ class Form
3
+ class Guesser
4
+ def initialize(model:, fields:, type:)
5
+ @model = model
6
+ @fields = fields
7
+ @type = type
8
+ end
9
+
10
+ def call
11
+ Schema::Guesser
12
+ .new(model: @model, fields: @fields, type: @type)
13
+ .assign_type { |attribute_name| attribute_type_for(attribute_name) }
14
+ .reject { |attribute_name| attribute_name == "id" }
15
+ .reject { |attribute_name| attribute_name == "created_at" }
16
+ .reject { |attribute_name| attribute_name == "updated_at" }
17
+ .call
18
+ end
19
+
20
+ private
21
+
22
+ def attribute_type_for(attribute_name)
23
+ @type.string
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ module Super
2
+ class Form
3
+ module InlineErrors
4
+ module_function
5
+
6
+ def error_messages(model_instance, column_or_association)
7
+ errable_fields(model_instance, column_or_association)
8
+ .flat_map { |field| Compatability.errable_fields(field) }
9
+ .flat_map { |field| model_instance.errors.full_messages_for(field) }
10
+ .uniq
11
+ end
12
+
13
+ def errable_fields(model_instance, column_or_association)
14
+ column_or_association = column_or_association.to_s
15
+ reflection = model_instance.class.reflect_on_association(column_or_association)
16
+ reflection ||= model_instance.class.reflections.values.find { |r| r.foreign_key == column_or_association }
17
+
18
+ if reflection
19
+ [reflection.name.to_s, reflection.foreign_key.to_s]
20
+ else
21
+ [column_or_association]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end