super 0.0.12 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +0 -1
  3. data/README.md +38 -46
  4. data/app/assets/javascripts/super/application.js +5747 -3803
  5. data/app/assets/stylesheets/super/application.css +114686 -71486
  6. data/app/controllers/super/application_controller.rb +44 -37
  7. data/app/controllers/super/substructure_controller.rb +276 -0
  8. data/app/helpers/super/form_builder_helper.rb +7 -0
  9. data/app/views/layouts/super/application.html.erb +4 -19
  10. data/app/views/super/application/_collection_header.html.erb +2 -2
  11. data/app/views/super/application/_display_actions.html.erb +1 -1
  12. data/app/views/super/application/_display_index.html.erb +2 -2
  13. data/app/views/super/application/_display_show.html.erb +1 -1
  14. data/app/views/super/application/_filter.html.erb +62 -2
  15. data/app/views/super/application/_form_field.html.erb +5 -0
  16. data/app/views/super/application/_layout.html.erb +1 -1
  17. data/app/views/super/application/_member_header.html.erb +2 -2
  18. data/app/views/super/application/_pagination.html.erb +1 -1
  19. data/app/views/super/application/_site_footer.html.erb +3 -0
  20. data/app/views/super/application/_site_header.html.erb +17 -0
  21. data/app/views/super/application/_sort_expression.html.erb +2 -2
  22. data/app/views/super/feather/README.md +0 -1
  23. data/frontend/super-frontend/dist/application.css +114686 -71486
  24. data/frontend/super-frontend/dist/application.js +5747 -3803
  25. data/lib/generators/super/install/install_generator.rb +0 -16
  26. data/lib/generators/super/install/templates/base_controller.rb.tt +0 -8
  27. data/lib/generators/super/resource/templates/resources_controller.rb.tt +4 -4
  28. data/lib/generators/super/webpacker/webpacker_generator.rb +9 -5
  29. data/lib/super.rb +5 -2
  30. data/lib/super/action_inquirer.rb +18 -3
  31. data/lib/super/assets.rb +44 -23
  32. data/lib/super/badge.rb +60 -0
  33. data/lib/super/cheat.rb +17 -0
  34. data/lib/super/compatibility.rb +19 -0
  35. data/lib/super/display/guesser.rb +3 -1
  36. data/lib/super/display/schema_types.rb +51 -2
  37. data/lib/super/error.rb +2 -0
  38. data/lib/super/filter.rb +1 -1
  39. data/lib/super/filter/form_object.rb +74 -48
  40. data/lib/super/filter/guesser.rb +2 -0
  41. data/lib/super/filter/operator.rb +90 -64
  42. data/lib/super/filter/schema_types.rb +63 -80
  43. data/lib/super/form/builder.rb +110 -27
  44. data/lib/super/form/field_transcript.rb +43 -0
  45. data/lib/super/form/guesser.rb +10 -1
  46. data/lib/super/form/schema_types.rb +73 -16
  47. data/lib/super/link.rb +38 -32
  48. data/lib/super/link_builder.rb +58 -0
  49. data/lib/super/navigation.rb +164 -0
  50. data/lib/super/pagination.rb +2 -44
  51. data/lib/super/reset.rb +22 -0
  52. data/lib/super/schema.rb +4 -0
  53. data/lib/super/useful/builder.rb +4 -4
  54. data/lib/super/version.rb +1 -1
  55. data/lib/tasks/super/cheat.rake +9 -0
  56. metadata +14 -19
  57. data/CONTRIBUTING.md +0 -56
  58. data/Rakefile +0 -36
  59. data/app/views/super/application/_filter_type_select.html.erb +0 -21
  60. data/app/views/super/application/_filter_type_text.html.erb +0 -18
  61. data/app/views/super/application/_filter_type_timestamp.html.erb +0 -24
  62. data/app/views/super/application/_form_field_checkbox.html.erb +0 -1
  63. data/app/views/super/application/_form_field_rich_text_area.html.erb +0 -1
  64. data/app/views/super/application/_form_field_select.html.erb +0 -1
  65. data/app/views/super/application/_form_field_text.html.erb +0 -1
  66. data/app/views/super/feather/_chevron_down.html +0 -1
  67. data/docs/cheat.md +0 -41
  68. data/lib/super/controls.rb +0 -22
  69. data/lib/super/controls/optional.rb +0 -113
  70. data/lib/super/controls/steps.rb +0 -106
  71. data/lib/super/controls/view.rb +0 -55
  72. data/lib/super/navigation/automatic.rb +0 -73
data/lib/super/error.rb CHANGED
@@ -28,6 +28,8 @@ 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
31
33
 
32
34
  class Enum < Error
33
35
  class ImpossibleValue < Enum; end
data/lib/super/filter.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Super
4
4
  class Filter
5
5
  def initialize
6
- @schema_type = Filter::SchemaTypes.new
6
+ @schema_type = SchemaTypes.new
7
7
  @fields = Schema::Fields.new
8
8
 
9
9
  yield(@fields, @schema_type)
@@ -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
@@ -2,112 +2,95 @@
2
2
 
3
3
  module Super
4
4
  class Filter
5
- # This schema type is used to configure the filtering form on your +#index+
6
- # action.
7
- #
8
- # The +operators:+ keyword argument can be left out in each case. There is
9
- # a default set of operators that are provided.
10
- #
11
5
  # Note: The constants under "Defined Under Namespace" are considered
12
6
  # private.
13
- #
14
- # class MemberDashboard
15
- # # ...
16
- #
17
- # def filter_schema
18
- # Super::Filter.new do |fields, type|
19
- # fields[:name] = type.text(operators: [
20
- # Super::Filter::Operator.eq,
21
- # Super::Filter::Operator.contain,
22
- # Super::Filter::Operator.ncontain,
23
- # Super::Filter::Operator.start,
24
- # Super::Filter::Operator.end,
25
- # ])
26
- # fields[:rank] = type.select(collection: Member.ranks.values)
27
- # fields[:position] = type.text(operators: [
28
- # Super::Filter::Operator.eq,
29
- # Super::Filter::Operator.neq,
30
- # Super::Filter::Operator.contain,
31
- # Super::Filter::Operator.ncontain,
32
- # ])
33
- # fields[:ship_id] = type.select(
34
- # collection: Ship.all.map { |s| ["#{s.name} (Ship ##{s.id})", s.id] },
35
- # )
36
- # fields[:created_at] = type.timestamp
37
- # fields[:updated_at] = type.timestamp
38
- # end
39
- # end
40
- #
41
- # # ...
42
- # end
43
7
  class SchemaTypes
44
- class Text
45
- def initialize(partial_path:, operators:)
46
- @partial_path = partial_path
47
- @operators = operators
48
- end
8
+ class OperatorList
9
+ include Enumerable
49
10
 
50
- attr_reader :operators
11
+ def initialize(*new_operators)
12
+ @operators = {}
13
+ @operator_transcript = {}
14
+ @fallback_transcript = nil
51
15
 
52
- def to_partial_path
53
- @partial_path
16
+ push(*new_operators)
54
17
  end
55
18
 
56
- def q
57
- [:q]
58
- end
59
- end
19
+ def push(*new_operators)
20
+ new_operators.flatten.map(&:dup).each do |new_operator|
21
+ new_identifier = new_operator.identifier.to_s
22
+
23
+ raise Error::AlreadyRegistered if @operators.key?(new_identifier)
60
24
 
61
- class Select
62
- def initialize(collection:, operators:)
63
- @collection = collection
64
- @operators = operators
25
+ @operators[new_identifier] = new_operator
26
+ end
27
+
28
+ nil
65
29
  end
66
30
 
67
- attr_reader :collection
68
- attr_reader :operators
31
+ alias add push
32
+
33
+ def each
34
+ return enum_for(:each) if !block_given?
69
35
 
70
- def to_partial_path
71
- "filter_type_select"
36
+ @operators.each do |identifier, operator|
37
+ yield(
38
+ OperatorWithFieldTranscript.new(
39
+ operator,
40
+ @operator_transcript[identifier] || @fallback_transcript
41
+ )
42
+ )
43
+ end
72
44
  end
73
45
 
74
- def q
75
- [:q]
46
+ def transcribe(operator_identifier = nil)
47
+ transcript = Form::FieldTranscript.new
48
+ yield transcript
49
+
50
+ if operator_identifier.nil?
51
+ @fallback_transcript = transcript
52
+ else
53
+ @operator_transcript[operator_identifier.to_s] = transcript
54
+ end
55
+
56
+ self
76
57
  end
77
58
  end
78
59
 
79
- class Timestamp
80
- def initialize(operators:)
81
- @operators = operators
60
+ class OperatorWithFieldTranscript
61
+ def initialize(operator, field_transcript)
62
+ @operator = operator
63
+ @field_transcript = field_transcript
82
64
  end
83
65
 
84
- attr_reader :operators
85
-
86
- def to_partial_path
87
- "filter_type_timestamp"
66
+ Super::Filter::Operator.instance_methods(false).each do |name|
67
+ delegate name, to: :@operator
88
68
  end
89
69
 
90
- def q
91
- [:q0, :q1]
92
- end
70
+ attr_reader :field_transcript
71
+ end
72
+
73
+ def use(*identifiers)
74
+ found_operators = identifiers.flatten.map { |id| Operator[id] }
75
+ OperatorList.new(*found_operators)
76
+ end
77
+
78
+ def select(collection)
79
+ use("eq", "null", "nnull")
80
+ .transcribe { |f| f.super.select(collection) }
93
81
  end
94
82
 
95
- def select(collection:, operators: Filter::Operator.select_defaults)
96
- Select.new(
97
- collection: collection,
98
- operators: operators
99
- )
83
+ def text
84
+ use("contain", "ncontain", "blank", "nblank")
100
85
  end
101
86
 
102
- def text(operators: Filter::Operator.text_defaults)
103
- Text.new(
104
- partial_path: "filter_type_text",
105
- operators: operators
106
- )
87
+ def timestamp
88
+ use("between", "null", "nnull")
89
+ .transcribe { |f| f.super.datetime_flatpickr }
107
90
  end
108
91
 
109
- def timestamp(operators: Filter::Operator.range_defaults)
110
- Timestamp.new(operators: operators)
92
+ def boolean
93
+ use("true", "false", "null", "nnull")
111
94
  end
112
95
  end
113
96
  end