super 0.0.14 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
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