super 0.0.14 → 0.18.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/app/assets/javascripts/super/application.js +148 -15
  4. data/app/assets/stylesheets/super/application.css +15 -22
  5. data/app/controllers/super/application_controller.rb +44 -42
  6. data/app/controllers/super/substructure_controller.rb +313 -0
  7. data/app/helpers/super/form_builder_helper.rb +7 -0
  8. data/app/views/layouts/super/application.html.erb +3 -21
  9. data/app/views/super/application/_collection_header.html.erb +2 -2
  10. data/app/views/super/application/_display_actions.html.erb +1 -1
  11. data/app/views/super/application/_display_index.html.erb +2 -2
  12. data/app/views/super/application/_display_show.html.erb +1 -1
  13. data/app/views/super/application/_filter.html.erb +62 -2
  14. data/app/views/super/application/_form_field.html.erb +5 -0
  15. data/app/views/super/application/_member_header.html.erb +2 -2
  16. data/app/views/super/application/_pagination.html.erb +1 -1
  17. data/app/views/super/application/_site_footer.html.erb +3 -0
  18. data/app/views/super/application/_site_header.html.erb +17 -0
  19. data/app/views/super/application/_sort_expression.html.erb +2 -2
  20. data/app/views/super/application/index.csv.erb +14 -0
  21. data/app/views/super/feather/README.md +0 -1
  22. data/frontend/super-frontend/dist/application.css +15 -22
  23. data/frontend/super-frontend/dist/application.js +148 -15
  24. data/lib/generators/super/install/install_generator.rb +0 -16
  25. data/lib/generators/super/install/templates/base_controller.rb.tt +0 -8
  26. data/lib/generators/super/resource/templates/resources_controller.rb.tt +4 -4
  27. data/lib/super/action_inquirer.rb +18 -3
  28. data/lib/super/badge.rb +60 -0
  29. data/lib/super/cheat.rb +6 -6
  30. data/lib/super/display/guesser.rb +1 -1
  31. data/lib/super/display/schema_types.rb +49 -1
  32. data/lib/super/display.rb +2 -1
  33. data/lib/super/error.rb +3 -0
  34. data/lib/super/filter/form_object.rb +74 -48
  35. data/lib/super/filter/guesser.rb +2 -0
  36. data/lib/super/filter/operator.rb +90 -64
  37. data/lib/super/filter/schema_types.rb +63 -80
  38. data/lib/super/filter.rb +1 -1
  39. data/lib/super/form/builder.rb +30 -39
  40. data/lib/super/form/field_transcript.rb +43 -0
  41. data/lib/super/form/guesser.rb +4 -4
  42. data/lib/super/form/schema_types.rb +66 -22
  43. data/lib/super/link.rb +2 -2
  44. data/lib/super/pagination.rb +2 -44
  45. data/lib/super/reset.rb +25 -0
  46. data/lib/super/schema.rb +4 -0
  47. data/lib/super/useful/builder.rb +4 -4
  48. data/lib/super/version.rb +1 -1
  49. data/lib/super.rb +3 -1
  50. data/lib/tasks/super/cheat.rake +1 -1
  51. metadata +25 -19
  52. data/app/views/super/application/_filter_type_select.html.erb +0 -21
  53. data/app/views/super/application/_filter_type_text.html.erb +0 -18
  54. data/app/views/super/application/_filter_type_timestamp.html.erb +0 -24
  55. data/app/views/super/application/_form_field_checkbox.html.erb +0 -1
  56. data/app/views/super/application/_form_field_flatpickr_date.html.erb +0 -8
  57. data/app/views/super/application/_form_field_flatpickr_datetime.html.erb +0 -8
  58. data/app/views/super/application/_form_field_flatpickr_time.html.erb +0 -8
  59. data/app/views/super/application/_form_field_rich_text_area.html.erb +0 -1
  60. data/app/views/super/application/_form_field_select.html.erb +0 -1
  61. data/app/views/super/application/_form_field_text.html.erb +0 -1
  62. data/app/views/super/feather/_chevron_down.html +0 -1
  63. data/docs/cheat.md +0 -41
  64. data/lib/super/controls/optional.rb +0 -113
  65. data/lib/super/controls/steps.rb +0 -106
  66. data/lib/super/controls/view.rb +0 -55
  67. data/lib/super/controls.rb +0 -22
@@ -19,12 +19,27 @@ module Super
19
19
  #
20
20
  def self.default_for_resources
21
21
  {
22
- read: %i[index show new edit],
23
- write: %i[create update destroy],
24
- delete: %i[destroy]
22
+ "read" => %w[index show new edit],
23
+ "write" => %w[create update destroy],
24
+ "delete" => %w[destroy],
25
25
  }
26
26
  end
27
27
 
28
+ # @return [ActionInquirer]
29
+ def self.index!; new(default_for_resources, "index"); end
30
+ # @return [ActionInquirer]
31
+ def self.show!; new(default_for_resources, "show"); end
32
+ # @return [ActionInquirer]
33
+ def self.new!; new(default_for_resources, "new"); end
34
+ # @return [ActionInquirer]
35
+ def self.edit!; new(default_for_resources, "edit"); end
36
+ # @return [ActionInquirer]
37
+ def self.create!; new(default_for_resources, "create"); end
38
+ # @return [ActionInquirer]
39
+ def self.update!; new(default_for_resources, "update"); end
40
+ # @return [ActionInquirer]
41
+ def self.destroy!; new(default_for_resources, "destroy"); end
42
+
28
43
  def initialize(categories_and_their_actions, action)
29
44
  @categories = categories_and_their_actions.keys.map(&:to_s)
30
45
  @actions = categories_and_their_actions.values.flatten.uniq.map(&:to_s)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Super
4
+ class Badge
5
+ STYLES = {
6
+ light: "bg-gray-100 text-black",
7
+ dark: "bg-gray-900 text-white",
8
+ red: "bg-red-700 text-white",
9
+ yellow: "bg-yellow-400 text-black",
10
+ green: "bg-green-700 text-white",
11
+ blue: "bg-blue-700 text-white",
12
+ purple: "bg-purple-800 text-white",
13
+ }
14
+
15
+ def initialize(text, styles: nil)
16
+ @text = text
17
+ @requested_styles = Array(styles.presence).flatten
18
+ end
19
+
20
+ def styles
21
+ return @styles if instance_variable_defined?(:@styles)
22
+ @styles =
23
+ if requested_styles.delete(:reset)
24
+ []
25
+ else
26
+ COMMON_STYLES
27
+ end
28
+
29
+ if requested_styles.empty?
30
+ @styles += [STYLES[:light]]
31
+ else
32
+ requested_styles.each do |style|
33
+ @styles +=
34
+ if STYLES.key?(style)
35
+ [STYLES[style]]
36
+ else
37
+ [style]
38
+ end
39
+ end
40
+ end
41
+
42
+ @styles
43
+ end
44
+
45
+ def to_s
46
+ ActionController::Base.helpers.content_tag(
47
+ :span,
48
+ @text,
49
+ class: styles.join(" ")
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ COMMON_STYLES = %w[rounded px-2 py-1 text-xs leading-none font-bold]
56
+ private_constant :COMMON_STYLES
57
+
58
+ attr_reader :requested_styles
59
+ end
60
+ end
data/lib/super/cheat.rb CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  module Super
4
4
  class Cheat
5
- def controls
6
- paths = %w[controls.rb controls/optional.rb controls/steps.rb controls/view.rb]
5
+ def controller
6
+ paths = %w[../../app/controllers/super/substructure_controller.rb]
7
7
  methods =
8
8
  paths
9
- .map { |f| File.read(File.join(__dir__, f)) }
10
- .flat_map { |content| content.scan(/^\s+def .*$/) }
11
- .map { |method| method.strip.sub(/^def /, "#") }
9
+ .map { |f| File.read(File.expand_path(f, __dir__)) }
10
+ .flat_map { |content| content.scan(/^\s+(?:helper_method )?def .*$/) }
11
+ .map { |method| method.strip.sub(/^(?:helper_method )?def /, "#") }
12
12
 
13
- puts "== Super::Controls"
13
+ puts "== Super::ApplicationController"
14
14
  puts methods.join("\n")
15
15
  end
16
16
  end
@@ -30,7 +30,7 @@ module Super
30
30
  when :time
31
31
  @type.time
32
32
  else
33
- @type.text
33
+ @type.string
34
34
  end
35
35
  end
36
36
  end
@@ -61,6 +61,47 @@ module Super
61
61
  end
62
62
  end
63
63
 
64
+ class Badge
65
+ extend Useful::Builder
66
+
67
+ def initialize(builder)
68
+ @builder = builder
69
+ @whens = {}
70
+ format_for_lookup(&:itself)
71
+ format_for_display(&:itself)
72
+ end
73
+
74
+ builder_with_block def when(*patterns, &block)
75
+ patterns.each do |pattern|
76
+ @whens[pattern] = block
77
+ end
78
+ end
79
+
80
+ builder_with_block def else(&block)
81
+ @else = block
82
+ end
83
+
84
+ builder_with_block def format_for_lookup(&block)
85
+ @format_for_lookup = block
86
+ end
87
+
88
+ builder_with_block def format_for_display(&block)
89
+ @format_for_display = block
90
+ end
91
+
92
+ def build
93
+ @builder.transform do |value|
94
+ lookup_value = @format_for_lookup.call(value)
95
+ block = @whens[lookup_value] || @else
96
+ Super::Badge.new(
97
+ @format_for_display.call(value),
98
+ styles: block&.call
99
+ )
100
+ end
101
+ @builder.build
102
+ end
103
+ end
104
+
64
105
  def initialize(fields:)
65
106
  @actions_called = false
66
107
  @fields = fields
@@ -89,7 +130,6 @@ module Super
89
130
  end
90
131
 
91
132
  def string; real(&:to_s); end
92
- alias text string
93
133
 
94
134
  def timestamp; real(&:to_s); end
95
135
  def time; real { |value| value.strftime("%H:%M:%S") }; end
@@ -100,6 +140,14 @@ module Super
100
140
  end
101
141
  end
102
142
 
143
+ def badge(*builder_methods)
144
+ builder_methods = %i[real ignore_nil column] if builder_methods.empty?
145
+ builder = builder_methods.each_with_object(Builder.new) do |builder_method, builder|
146
+ builder.public_send(builder_method)
147
+ end
148
+ Badge.new(builder)
149
+ end
150
+
103
151
  def actions
104
152
  @actions_called = true
105
153
  Builder.new.computed.none.transform do
data/lib/super/display.rb CHANGED
@@ -32,10 +32,11 @@ module Super
32
32
  yield(@fields, @schema_types)
33
33
  end
34
34
 
35
- def apply(action:)
35
+ def apply(action:, format:)
36
36
  @action_inquirer = action
37
37
  return self if !@action_inquirer.index?
38
38
  return self if @schema_types.actions_called?
39
+ return self if !format.html?
39
40
  @fields[:actions] = @schema_types.actions
40
41
  self
41
42
  end
data/lib/super/error.rb CHANGED
@@ -28,6 +28,9 @@ module Super
28
28
  # a more specific error
29
29
  class Initalization < Error; end
30
30
  class ArgumentError < Error; end
31
+ class AlreadyRegistered < Error; end
32
+ class AlreadyTranscribed < Error; end
33
+ class NotImplementedError < Error; end
31
34
 
32
35
  class Enum < Error
33
36
  class ImpossibleValue < Enum; end
@@ -3,58 +3,95 @@
3
3
  module Super
4
4
  class Filter
5
5
  class FormObject
6
- class FilterFormField
7
- def initialize(humanized_field_name:, field_name:, type:, params:)
8
- @humanized_field_name = humanized_field_name
6
+ class AttributeForm
7
+ def initialize(model:, field_name:, operators:, params:)
8
+ @model = model
9
9
  @field_name = field_name
10
- @field_type = type
11
- @params = params
10
+ @operators = operators
11
+ @params = params || {}
12
+ end
13
+
14
+ attr_reader :model
15
+ attr_reader :field_name
16
+ attr_reader :operators
17
+ attr_reader :params
18
+
19
+ def each_operator
20
+ return enum_for(:each_operator) if !block_given?
21
+
22
+ @operators.each do |operator|
23
+ operator_form = OperatorForm.new(
24
+ operator: operator,
25
+ params: @params[operator.identifier]
26
+ )
27
+
28
+ yield(operator_form)
29
+ end
30
+ end
31
+
32
+ def humanized_attribute_name
33
+ @model.human_attribute_name(@field_name)
34
+ end
35
+ end
36
+
37
+ class OperatorForm
38
+ NULLARY = :_apply
39
+
40
+ def initialize(operator:, params:)
41
+ @operator = operator
42
+ @params = params || {}
43
+ query_parameter_keys = operator.query_parameter_keys
44
+ query_parameter_keys = [NULLARY] if query_parameter_keys.empty?
12
45
  @specified_values =
13
- type.q
14
- .map do |query_field_name|
15
- [
16
- query_field_name,
17
- (params || {})[query_field_name],
18
- ]
19
- end
20
- .to_h
46
+ query_parameter_keys
47
+ .map { |key| [key, @params[key].presence&.strip] }
48
+ .to_h
21
49
 
22
50
  @specified_values.each do |key, value|
23
51
  define_singleton_method(key) { value }
24
52
  end
25
53
  end
26
54
 
27
- attr_reader :humanized_field_name
28
- attr_reader :field_name
29
- attr_reader :field_type
55
+ def specified?
56
+ @specified_values.any? { |_key, value| value }
57
+ end
58
+
59
+ attr_reader :operator
30
60
  attr_reader :specified_values
31
61
 
32
- def op
33
- (@params || {})[:op]
62
+ def identifier
63
+ @operator.identifier
34
64
  end
35
65
 
36
- def operators
37
- @field_type.operators
38
- .map { |o| [o.name, o.identifier] }
39
- .to_h
40
- end
66
+ def each_field
67
+ return enum_for(:each_field) if !block_given?
41
68
 
42
- def to_partial_path
43
- @field_type.to_partial_path
69
+ @specified_values.each do |key, _value|
70
+ yield(key)
71
+ end
44
72
  end
45
73
  end
46
74
 
47
75
  def initialize(model:, params:, schema:)
48
76
  @model = model
49
- @params = params
77
+ @params = params || {}
50
78
  @schema = schema
51
79
 
52
80
  @form_fields = {}
53
81
  end
54
82
 
55
- def each_field
56
- @schema.fields.each do |field_name, _field_type|
57
- yield(form_field_for(field_name))
83
+ def each_attribute
84
+ return enum_for(:each_attribute) if !block_given?
85
+
86
+ @schema.fields.each do |field_name, field_operators|
87
+ attribute_form = AttributeForm.new(
88
+ model: @model,
89
+ field_name: field_name,
90
+ operators: field_operators,
91
+ params: @params[field_name]
92
+ )
93
+
94
+ yield(attribute_form)
58
95
  end
59
96
  end
60
97
 
@@ -63,32 +100,21 @@ module Super
63
100
  end
64
101
 
65
102
  def apply_changes(relation)
66
- each_field do |form_field|
67
- next if form_field.specified_values.values.map(&:to_s).map(&:strip).all? { |specified_value| specified_value == "" }
68
- next if !Super::Filter::Operator.registry.key?(form_field.op)
103
+ each_attribute do |attribute_form|
104
+ attribute_form.each_operator do |operator_form|
105
+ next if operator_form.specified_values.values.map(&:to_s).map(&:presence).none?
69
106
 
70
- operator = Super::Filter::Operator.registry[form_field.op]
71
- updated_relation = operator.filter(relation, form_field.field_name, *form_field.specified_values.values)
107
+ operator_behavior = operator_form.operator.behavior
108
+ updated_relation = operator_behavior.call(relation, attribute_form.field_name, **operator_form.specified_values)
72
109
 
73
- if updated_relation.is_a?(ActiveRecord::Relation)
74
- relation = updated_relation
110
+ if updated_relation.is_a?(ActiveRecord::Relation)
111
+ relation = updated_relation
112
+ end
75
113
  end
76
114
  end
77
115
 
78
116
  relation
79
117
  end
80
-
81
- private
82
-
83
- def form_field_for(field_name)
84
- @form_fields[field_name] ||=
85
- FilterFormField.new(
86
- humanized_field_name: @model.human_attribute_name(field_name),
87
- field_name: field_name,
88
- type: @schema.fields[field_name],
89
- params: (@params || {})[field_name]
90
- )
91
- end
92
118
  end
93
119
  end
94
120
  end
@@ -23,6 +23,8 @@ module Super
23
23
  case type
24
24
  when :datetime
25
25
  @type.timestamp
26
+ when :boolean
27
+ @type.boolean
26
28
  else
27
29
  @type.text
28
30
  end
@@ -2,100 +2,126 @@
2
2
 
3
3
  module Super
4
4
  class Filter
5
- module Operator
6
- class Definition
7
- def initialize(identifier, name, filter)
8
- @identifier = identifier
9
- @name = name
10
- @filter = filter
11
- end
12
-
13
- attr_reader :identifier
14
- attr_reader :name
15
-
16
- def filter(*args)
17
- @filter.call(args)
18
- end
19
- end
20
-
5
+ class Operator
21
6
  class << self
22
7
  def registry
23
8
  @registry ||= {}
24
9
  end
25
10
 
26
- def range_defaults
27
- [
28
- registry["between"],
29
- ]
11
+ def [](key)
12
+ registry.fetch(key.to_s)
30
13
  end
31
14
 
32
- def select_defaults
33
- [
34
- registry["eq"],
35
- registry["neq"],
36
- ]
15
+ def register(identifier, operator)
16
+ identifier = identifier.to_s
17
+ if registry.key?(identifier)
18
+ raise Error::AlreadyRegistered, "Already registered: #{identifier}"
19
+ end
20
+
21
+ registry[identifier] = operator
37
22
  end
38
23
 
39
- def text_defaults
40
- [
41
- registry["eq"],
42
- registry["neq"],
43
- registry["contain"],
44
- registry["ncontain"],
45
- registry["start"],
46
- registry["end"],
47
- ]
24
+ def define(identifier, display, &block)
25
+ operator = new(identifier, display, &block).freeze
26
+ register(identifier, operator)
27
+ operator
48
28
  end
29
+ end
49
30
 
50
- def define(identifier, name, &filter)
51
- identifier = identifier.to_s
52
- name = name.to_s
31
+ def initialize(identifier, display, &behavior)
32
+ @identifier = identifier.to_s
33
+ @humanized_operator_name = display
34
+ self.behavior = behavior
35
+ end
36
+
37
+ def behavior=(behavior)
38
+ behavior_params = behavior.parameters
39
+ if behavior_params.size < 2
40
+ raise Error::ArgumentError, "Operator behavior must include `column_name` and `relation`"
41
+ end
42
+ if behavior_params[0][0] != :req && behavior_params[0][0] != :opt
43
+ raise Error::ArgumentError, "First argument `relation` must be a required, positional argument"
44
+ end
45
+ if behavior_params[1][0] != :req && behavior_params[1][0] != :opt
46
+ raise Error::ArgumentError, "Second argument `column_name` must be a required, positional argument"
47
+ end
48
+ if !behavior_params[2..-1].all? { |(type, _name)| type == :keyreq }
49
+ raise Error::ArgumentError, "All query parameter keys must be required, keyword arguments"
50
+ end
51
+ @behavior = behavior
52
+ @query_parameter_keys = behavior_params[2..-1].map(&:last)
53
+ end
53
54
 
54
- definition = Definition.new(identifier, name, filter)
55
+ attr_reader :identifier
56
+ attr_reader :query_parameter_keys
57
+ attr_reader :humanized_operator_name
55
58
 
56
- registry[identifier] = definition
59
+ def behavior(&block)
60
+ self.behavior = block if block_given?
61
+ @behavior
62
+ end
57
63
 
58
- define_singleton_method(identifier) do
59
- registry[identifier]
60
- end
61
- end
64
+ define("eq", "Equals") do |relation, field, q:|
65
+ relation.where(field => q)
62
66
  end
63
67
 
64
- define("eq", "equals") do |relation, field, query|
65
- relation.where(field => query)
68
+ define("neq", "Doesn't equal") do |relation, field, q:|
69
+ relation.where.not(field => q)
66
70
  end
67
71
 
68
- define("neq", "doesn't equal") do |relation, field, query|
69
- relation.where.not(field => query)
72
+ define("null", "Is NULL") do |relation, field|
73
+ relation.where(field => nil)
70
74
  end
71
75
 
72
- define("contain", "contains") do |relation, field, query|
73
- query = "%#{Compatability.sanitize_sql_like(query)}%"
74
- relation.where("#{field} LIKE ?", "%#{query}%")
76
+ define("nnull", "Isn't NULL") do |relation, field|
77
+ relation.where.not(field => nil)
75
78
  end
76
79
 
77
- define("ncontain", "doesn't contain") do |relation, field, query|
78
- query = "%#{Compatability.sanitize_sql_like(query)}%"
79
- relation.where("#{field} NOT LIKE ?", query)
80
+ define("true", "Is true") do |relation, field|
81
+ relation.where(field => true)
82
+ end
83
+
84
+ define("false", "Is false") do |relation, field|
85
+ relation.where(field => false)
86
+ end
87
+
88
+ define("empty", "Is empty") do |relation, field|
89
+ relation.where(field => "")
80
90
  end
81
91
 
82
- define("start", "starts with") do |relation, field, query|
83
- query = "#{Compatability.sanitize_sql_like(query)}%"
84
- relation.where("#{field} LIKE ?", query)
92
+ define("nempty", "Isn't empty") do |relation, field|
93
+ relation.where.not(field => "")
85
94
  end
86
95
 
87
- define("end", "ends with") do |relation, field, query|
88
- query = "%#{Compatability.sanitize_sql_like(query)}"
89
- relation.where("#{field} LIKE ?", query)
96
+ define("blank", "Is blank") do |relation, field|
97
+ relation.where(field => [nil, ""])
98
+ end
99
+
100
+ define("nblank", "Isn't blank") do |relation, field|
101
+ relation.where.not(field => [nil, ""])
102
+ end
103
+
104
+ define("contain", "Contains") do |relation, field, q:|
105
+ query = "%#{Compatability.sanitize_sql_like(q)}%"
106
+ if relation.connection.adapter_name == "PostgreSQL"
107
+ relation.where("#{field} ILIKE ?", query)
108
+ else
109
+ relation.where("#{field} LIKE ?", query)
110
+ end
111
+ end
112
+
113
+ define("ncontain", "Doesn't contain") do |relation, field, q:|
114
+ query = "%#{Compatability.sanitize_sql_like(q)}%"
115
+ relation.where("#{field} NOT LIKE ?", query)
90
116
  end
91
117
 
92
- define("between", "between") do |relation, field, query0, query1|
93
- if query0.present?
94
- relation = relation.where("#{field} >= ?", query0)
118
+ define("between", "Between") do |relation, field, q0:, q1:|
119
+ if q0.present?
120
+ relation = relation.where("#{field} >= ?", q0)
95
121
  end
96
122
 
97
- if query1.present?
98
- relation = relation.where("#{field} <= ?", query1)
123
+ if q1.present?
124
+ relation = relation.where("#{field} <= ?", q1)
99
125
  end
100
126
 
101
127
  relation