super 0.0.5 → 0.0.10

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.
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