warped 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +25 -0
  3. data/Gemfile +0 -2
  4. data/Gemfile.lock +25 -19
  5. data/README.md +116 -270
  6. data/app/assets/config/warped_manifest.js +2 -0
  7. data/app/assets/javascript/warped/controllers/filter_controller.js +76 -0
  8. data/app/assets/javascript/warped/controllers/filters_controller.js +21 -0
  9. data/app/assets/javascript/warped/index.js +2 -0
  10. data/app/assets/stylesheets/warped/application.css +15 -0
  11. data/app/assets/stylesheets/warped/base.css +23 -0
  12. data/app/assets/stylesheets/warped/filters.css +115 -0
  13. data/app/assets/stylesheets/warped/pagination.css +74 -0
  14. data/app/assets/stylesheets/warped/search.css +33 -0
  15. data/app/assets/stylesheets/warped/table.css +114 -0
  16. data/app/views/warped/_actions.html.erb +9 -0
  17. data/app/views/warped/_cell.html.erb +3 -0
  18. data/app/views/warped/_column.html.erb +35 -0
  19. data/app/views/warped/_filters.html.erb +21 -0
  20. data/app/views/warped/_hidden_fields.html.erb +19 -0
  21. data/app/views/warped/_pagination.html.erb +34 -0
  22. data/app/views/warped/_row.html.erb +19 -0
  23. data/app/views/warped/_search.html.erb +21 -0
  24. data/app/views/warped/_table.html.erb +52 -0
  25. data/app/views/warped/filters/_filter.html.erb +40 -0
  26. data/config/importmap.rb +3 -0
  27. data/docs/controllers/FILTERABLE.md +193 -0
  28. data/docs/controllers/PAGEABLE.md +70 -0
  29. data/docs/controllers/README.md +8 -0
  30. data/docs/controllers/SEARCHABLE.md +95 -0
  31. data/docs/controllers/SORTABLE.md +94 -0
  32. data/docs/controllers/TABULATABLE.md +28 -0
  33. data/docs/controllers/views/PARTIALS.md +285 -0
  34. data/docs/jobs/README.md +22 -0
  35. data/docs/services/README.md +81 -0
  36. data/lib/generators/warped/install_generator.rb +1 -1
  37. data/lib/warped/api/filter/base/value.rb +52 -0
  38. data/lib/warped/api/filter/base.rb +84 -0
  39. data/lib/warped/api/filter/boolean.rb +41 -0
  40. data/lib/warped/api/filter/date.rb +26 -0
  41. data/lib/warped/api/filter/date_time.rb +32 -0
  42. data/lib/warped/api/filter/decimal.rb +31 -0
  43. data/lib/warped/api/filter/factory.rb +38 -0
  44. data/lib/warped/api/filter/integer.rb +38 -0
  45. data/lib/warped/api/filter/string.rb +25 -0
  46. data/lib/warped/api/filter/time.rb +25 -0
  47. data/lib/warped/api/filter.rb +14 -0
  48. data/lib/warped/api/sort/value.rb +40 -0
  49. data/lib/warped/api/sort.rb +65 -0
  50. data/lib/warped/controllers/filterable/ui.rb +46 -0
  51. data/lib/warped/controllers/filterable.rb +79 -42
  52. data/lib/warped/controllers/pageable/ui.rb +70 -0
  53. data/lib/warped/controllers/pageable.rb +11 -11
  54. data/lib/warped/controllers/searchable/ui.rb +37 -0
  55. data/lib/warped/controllers/searchable.rb +2 -0
  56. data/lib/warped/controllers/sortable/ui.rb +53 -0
  57. data/lib/warped/controllers/sortable.rb +53 -33
  58. data/lib/warped/controllers/tabulatable/ui.rb +54 -0
  59. data/lib/warped/controllers/tabulatable.rb +13 -27
  60. data/lib/warped/emails/components/align.rb +21 -0
  61. data/lib/warped/emails/components/base.rb +116 -0
  62. data/lib/warped/emails/components/button.rb +58 -0
  63. data/lib/warped/emails/components/divider.rb +15 -0
  64. data/lib/warped/emails/components/heading.rb +65 -0
  65. data/lib/warped/emails/components/layouts/columns.rb +36 -0
  66. data/lib/warped/emails/components/layouts/cta.rb +38 -0
  67. data/lib/warped/emails/components/layouts/main.rb +34 -0
  68. data/lib/warped/emails/components/link.rb +36 -0
  69. data/lib/warped/emails/components/spacer.rb +15 -0
  70. data/lib/warped/emails/components/stepper.rb +104 -0
  71. data/lib/warped/emails/components/table.rb +37 -0
  72. data/lib/warped/emails/components/text.rb +67 -0
  73. data/lib/warped/emails/helpers.rb +26 -0
  74. data/lib/warped/emails/slottable.rb +61 -0
  75. data/lib/warped/emails/styleable.rb +160 -0
  76. data/lib/warped/engine.rb +19 -0
  77. data/lib/warped/queries/filter.rb +32 -12
  78. data/lib/warped/table/action.rb +33 -0
  79. data/lib/warped/table/column.rb +34 -0
  80. data/lib/warped/version.rb +1 -1
  81. data/lib/warped.rb +2 -0
  82. data/warped.gemspec +1 -1
  83. metadata +73 -7
  84. data/lib/warped/emails/.keep +0 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ module Layouts
6
+ class Main < Base
7
+ variant do
8
+ base do
9
+ [
10
+ "font-family: 'Open Sans', 'Roboto', 'Segoe UI', Arial, sans-serif",
11
+ "max-width: 600px", "margin: 20px auto 0 auto",
12
+ "padding: 20px 30px", "background-color: #fff",
13
+ "border-radius: 5px", "box-shadow: 0 0 10px rgba(0, 0, 0, 0.1)"
14
+ ]
15
+ end
16
+ end
17
+
18
+ slots_one :header
19
+ slots_one :main
20
+ slots_one :footer
21
+
22
+ def template(&)
23
+ tag.div(style:) do
24
+ content_tag(:div, style: "vertical-align: middle;") do
25
+ concat tag.header(header)
26
+ concat tag.main(main)
27
+ concat tag.footer(footer)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ class Link < Text
6
+ variant do
7
+ base { ["text-decoration: underline"] }
8
+ end
9
+
10
+ default_variant size: :md, color: :info, display: :inline
11
+
12
+ def initialize(text = nil, href, size: nil, color: nil, display: nil)
13
+ super()
14
+ @text = text
15
+ @href = href
16
+ @size = size
17
+ @color = color
18
+ @display = display
19
+ end
20
+
21
+ def text
22
+ content.presence || @text
23
+ end
24
+
25
+ def template
26
+ style = style(size:, color:, display:)
27
+
28
+ tag.a(text, href:, style:)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :href, :size, :color, :display
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ class Spacer < Base
6
+ variant do
7
+ base { "height: 20px" }
8
+ end
9
+
10
+ def template(&)
11
+ tag.div(nil, style:)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ class Stepper < Base
6
+ variant do
7
+ base { "padding: 20px;" }
8
+ end
9
+
10
+ variant :step do
11
+ base do
12
+ ["border-radius: 100%;", "width: #{width}%",
13
+ "max-width: #{max_width}px", "height: #{height}px",
14
+ "text-align: center", "line-height: #{height}px",
15
+ "display: inline-block", "font-size: #{height / 2}px"]
16
+ end
17
+
18
+ state do
19
+ active { ["background-color: #007bff", "color: #fff"] }
20
+ inactive { ["background-color: #fff", "color: #007bff"] }
21
+ end
22
+ end
23
+
24
+ variant :step_divider do
25
+ base do
26
+ ["height: 3px", "border-radius: 3px", "width: 100%"]
27
+ end
28
+
29
+ state do
30
+ active { ["background-color: #007bff;"] }
31
+ inactive { ["background-color: #ddd;"] }
32
+ end
33
+ end
34
+
35
+ variant :table do
36
+ base do
37
+ ["display: inline-block", "min-width: 30px", "width: #{width}%",
38
+ "height: #{height}px", "border-collapse: collapse", "vertical-align: middle"]
39
+ end
40
+ end
41
+
42
+ default_variant size: :md, color: :info, display: :block
43
+
44
+ def initialize(steps:, current_step: 1)
45
+ super()
46
+ @steps = steps
47
+ @current_step = current_step
48
+ end
49
+
50
+ def template
51
+ tag.div(style: "width: 100%") do
52
+ capture do
53
+ (1..steps).map do |step|
54
+ if step <= current_step
55
+ concat step(step, state: :active)
56
+ else
57
+ concat step(step, state: :inactive)
58
+ end
59
+
60
+ if step < steps
61
+ concat divider(state: step <= current_step ? :active : :inactive)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ attr_reader :steps, :current_step
71
+
72
+ def step(number, state:)
73
+ tag.div(style: "#{style(:step, state:)}; width: #{width}%;") do
74
+ tag.span(number)
75
+ end
76
+ end
77
+
78
+ def divider(state:)
79
+ tag.table(style: style(:table)) do
80
+ tag.tbody(style: "width: 100%; height: 100%; display: table") do
81
+ tag.tr(style: "width: 100%; height: 100%") do
82
+ tag.td(valign: :middle,
83
+ style: "vertical-align: middle; text-align: center; height: 100%; width: 100%; padding: 0 3px") do
84
+ tag.div(style: style(:step_divider, state:))
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def width
92
+ (100 / (steps + (steps - 1))).round(4) - 0.8
93
+ end
94
+
95
+ def max_width
96
+ 50
97
+ end
98
+
99
+ def height
100
+ 50
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ class Table < Base
6
+ variant do
7
+ base do
8
+ ["border-collapse: collapse"]
9
+ end
10
+ end
11
+
12
+ variant :row do
13
+ base { ["border-bottom: 1px solid #f0f0f0"] }
14
+ end
15
+
16
+ variant :header do
17
+ base { ["background-color: #f9f9f9"] }
18
+ end
19
+
20
+ slots_one :header
21
+ slots_many :rows
22
+
23
+ def template
24
+ raise ArgumentError, "at least one row must be passed to the table" if rows.empty?
25
+
26
+ content_tag(:table, style:) do
27
+ concat content_tag(:thead, header, style: style(:header))
28
+ concat content_tag(:tbody) do
29
+ rows.each do |row|
30
+ concat content_tag(:tr, row, style: style(:row))
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ class Text < Base
6
+ variant do
7
+ size do
8
+ xs { ["font-size: 11px", "line-height: 16px"] }
9
+ sm { ["font-size: 13px", "line-height: 16px"] }
10
+ md { ["font-size: 16px", "line-height: 24px"] }
11
+ lg { ["font-size: 19px", "line-height: 24px"] }
12
+ end
13
+
14
+ color do
15
+ regular { "color: #414750" }
16
+ placeholder { "color: #8B939F" }
17
+ info { "color: #1C51A4" }
18
+ success { "color: #60830D" }
19
+ warning { "color: #82620F" }
20
+ error { "color: #AB2816" }
21
+ end
22
+
23
+ align do
24
+ left { "text-align: left" }
25
+ right { "text-align: right" }
26
+ center { "text-align: center" }
27
+ end
28
+
29
+ weight do
30
+ regular { "font-weight: 400" }
31
+ bold { "font-weight: 700" }
32
+ end
33
+
34
+ display do
35
+ block { "display: block" }
36
+ inline { "display: inline" }
37
+ end
38
+ end
39
+
40
+ default_variant size: :md, color: :regular, weight: :regular, display: :block
41
+
42
+ def initialize(text = nil, size: nil, color: nil, weight: nil, align: nil, display: nil)
43
+ super()
44
+ @text = text
45
+ @size = size
46
+ @color = color
47
+ @weight = weight
48
+ @align = align
49
+ @display = display
50
+ end
51
+
52
+ def text
53
+ content.presence || @text
54
+ end
55
+
56
+ def template(&)
57
+ style = style(size:, color:, weight:, align:, display:)
58
+
59
+ tag.p(text, style:)
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :size, :color, :weight, :align, :display
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Warped
6
+ module Emails
7
+ module Helpers
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ helper_method :frontend_url
12
+ end
13
+
14
+ def frontend_url(path = [])
15
+ frontend_path = Array(path)
16
+
17
+ url = base_url
18
+ path ? [url, *frontend_path].join("/").gsub(%r{(?<!:)//}, "/") : url
19
+ end
20
+
21
+ def base_url
22
+ ENV["FRONTEND_URL"].presence || ENV["SERVER_HOST"].presence || "http://localhost:3000"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warped
4
+ module Emails
5
+ module Slottable
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def slots_one(name)
12
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
+ # def with_header(&block)
14
+ # slots[:one][:header] = block
15
+ # end
16
+ #
17
+ # def header
18
+ # capture(&slots[:one][:header]) if slots[:one][:header]
19
+ # end
20
+ def with_#{name}(&block)
21
+ slots[:one][:#{name}] = block
22
+ end
23
+
24
+ def #{name}
25
+ capture(&slots[:one][:#{name}]) if slots[:one][:#{name}]
26
+ end
27
+ RUBY
28
+ end
29
+
30
+ def slots_many(name)
31
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
32
+ # def with_column(&block)
33
+ # slots[:many][:columns] ||= []
34
+ # slots[:many][:columns] << block
35
+ # end
36
+ #
37
+ # def columns
38
+ # return [] if slots[:many][:columns].empty?
39
+ #
40
+ # slots[:many][:columns].map { |block| capture(&block) }
41
+ # end
42
+ def with_#{name.to_s.singularize}(&block)
43
+ slots[:many][:#{name}] ||= []
44
+ slots[:many][:#{name}] << block
45
+ end
46
+
47
+ def #{name}
48
+ return [] if slots[:many][:#{name}].empty?
49
+
50
+ slots[:many][:#{name}].map { |block| capture(&block) }
51
+ end
52
+ RUBY
53
+ end
54
+ end
55
+
56
+ def slots
57
+ @slots ||= { one: {}, many: {} }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/enumerable"
4
+ require "active_support/core_ext/hash/deep_merge"
5
+ require "active_support/core_ext/module/delegation"
6
+ require "active_support/core_ext/object/deep_dup"
7
+
8
+ module Warped
9
+ module Emails
10
+ module Styleable
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+ attr_writer :default_variants, :base_styles, :variants
17
+
18
+ def inherited(subclass)
19
+ super
20
+
21
+ subclass.default_variants = default_variants.deep_dup.deep_merge!(subclass.default_variants)
22
+ subclass.base_styles = base_styles.deep_dup.deep_merge!(subclass.base_styles)
23
+ subclass.variants = variants.deep_dup.deep_merge!(subclass.variants)
24
+ end
25
+
26
+ def default_variants
27
+ @default_variants ||= {}
28
+ end
29
+
30
+ def base_styles
31
+ @base_styles ||= {}
32
+ end
33
+
34
+ def variants
35
+ @variants ||= {}
36
+ end
37
+
38
+ def variant(name = :_base_variant, &)
39
+ raise ArgumentError, "You must provide a block" unless block_given?
40
+
41
+ variant_builder = VariantBuilder.new(self, variant_name: name)
42
+ variant_builder.compile_variants(&)
43
+ end
44
+
45
+ def default_variant(variant_name = :_base_variant, **kwargs)
46
+ raise ArgumentError, "You must provide a hash" unless kwargs.is_a?(Hash)
47
+
48
+ self.default_variants ||= {}
49
+ self.default_variants[variant_name] ||= {}
50
+ self.default_variants[variant_name].merge!(kwargs)
51
+ end
52
+ end
53
+
54
+ def default_variants
55
+ self.class.default_variants
56
+ end
57
+
58
+ def base_styles
59
+ self.class.base_styles
60
+ end
61
+
62
+ def variants
63
+ self.class.variants
64
+ end
65
+
66
+ def style(variant_name = :_base_variant, **kwargs)
67
+ base_style_arr = base_block_value(variant_name)
68
+
69
+ validate_variants!(variant_name, **kwargs)
70
+
71
+ default_variants[variant_name] ||= {}
72
+ default_variants[variant_name].merge(kwargs.compact_blank).each do |group_name, subvariant_name|
73
+ subvariant_arr = variant_block_value(variant_name, group_name, subvariant_name)
74
+ base_style_arr.concat(subvariant_arr)
75
+ end
76
+
77
+ base_style_arr.join("; ")
78
+ end
79
+
80
+ # @!visibility private
81
+ class VariantBuilder < BasicObject
82
+ delegate :variants, to: :@component
83
+
84
+ def initialize(component, variant_name: :_base_variant, group_name: nil)
85
+ @component = component
86
+ @group_name = group_name
87
+ @variant_name = variant_name
88
+ end
89
+
90
+ def compile_variants(&)
91
+ instance_eval(&)
92
+ end
93
+
94
+ def method_missing(name, &)
95
+ return super unless ::Kernel.block_given?
96
+
97
+ if !@group_name && name == :base
98
+ _define_base_style(&)
99
+ elsif !@group_name
100
+ _define_variant_group(name, &)
101
+ else
102
+ _define_subvariant(name, &)
103
+ end
104
+ end
105
+
106
+ def respond_to_missing?(...) = true
107
+
108
+ private
109
+
110
+ def _define_variant_group(name, &)
111
+ VariantBuilder.new(@component, variant_name: @variant_name, group_name: name).compile_variants(&)
112
+ end
113
+
114
+ def _define_subvariant(name, &block)
115
+ variants[@variant_name] ||= {}
116
+ variants[@variant_name][@group_name] ||= {}
117
+ variants[@variant_name][@group_name][name] = block
118
+ end
119
+
120
+ def _define_base_style(&block)
121
+ @component.base_styles[@variant_name] = block
122
+ end
123
+ end
124
+
125
+ private_constant :VariantBuilder
126
+
127
+ private
128
+
129
+ def base_block_value(variant_name)
130
+ return [] unless base_styles.key?(variant_name.to_sym)
131
+
132
+ base_block = base_styles[variant_name.to_sym]
133
+ Array.wrap(instance_eval(&base_block))
134
+ end
135
+
136
+ def variant_block_value(variant_name, group_name, subvariant_name)
137
+ return [] if subvariant_name.nil?
138
+
139
+ variant_block = self.class.variants[variant_name][group_name.to_sym][subvariant_name.to_sym]
140
+ Array.wrap(instance_eval(&variant_block))
141
+ end
142
+
143
+ def validate_variants!(name, **kwargs)
144
+ kwargs.each do |group_name, variant_name|
145
+ next if variant_name.nil?
146
+
147
+ unless variants[name].key?(group_name.to_sym)
148
+ raise ArgumentError,
149
+ "Invalid variant group: #{group_name}"
150
+ end
151
+
152
+ unless variants[name][group_name.to_sym].key?(variant_name.to_sym)
153
+ raise ArgumentError,
154
+ "Invalid variant name: #{variant_name}"
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module Warped
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace Warped
8
+
9
+ initializer "warped.assets" do
10
+ if Rails.application.config.respond_to?(:assets)
11
+ Rails.application.config.assets.precompile += %w[warped_manifest.js]
12
+ end
13
+ end
14
+
15
+ initializer "warped.importmap", before: "importmap" do |app|
16
+ app.config.importmap.paths << Engine.root.join("config/importmap.rb") if Rails.application.respond_to?(:importmap)
17
+ end
18
+ end
19
+ end
@@ -10,22 +10,22 @@ module Warped
10
10
  # This class provides a simple interface for filtering a scope by a set of conditions.
11
11
  # The conditions are passed as an array of hashes, where each hash contains the following
12
12
  # keys:
13
- # - +relation+: the relation to use for the filter (e.g. "=", ">", "in", etc.)
13
+ # - +relation+: the relation to use for the filter (e.g. "eq", "gt", "in", etc.)
14
14
  # - +field+: the field to filter by
15
15
  # - +value+: the value to filter by
16
16
  #
17
17
  #
18
18
  # @example Filter a scope by a set of conditions
19
19
  # Warped::Queries::Filter.call(User.all, filter_conditions: [
20
- # { relation: "=", field: :first_name, value: "John" },
21
- # { relation: ">", field: :age, value: 18 }
20
+ # { relation: "eq", field: :first_name, value: "John" },
21
+ # { relation: "gt", field: :age, value: 18 }
22
22
  # ])
23
23
  # # => #<ActiveRecord::Relation [...]>
24
24
  #
25
25
  # @see RELATIONS
26
26
  # To see the list of available relations, check the +RELATIONS+ constant.
27
27
  class Filter
28
- RELATIONS = %w[= != > >= < <= between in not_in starts_with ends_with contains is_null is_not_null].freeze
28
+ RELATIONS = ::Warped::Filter::Base::RELATIONS.freeze
29
29
 
30
30
  # @param scope [ActiveRecord::Relation] the scope to filter
31
31
  # @param filter_conditions [Array<Hash>] the conditions to filter by
@@ -66,26 +66,46 @@ module Warped
66
66
 
67
67
  def filtered_scope(scope, relation, field, value)
68
68
  case relation
69
- when "=", "in"
69
+ when "eq"
70
70
  scope.where(field => value)
71
- when "!=", "not_in"
71
+ when "in"
72
+ scope.where(field => Array.wrap(value))
73
+ when "neq", "not_in"
72
74
  scope.where.not(field => value)
73
75
  when "between"
74
- scope.where("#{field} BETWEEN ? AND ?", value.first, value.last)
76
+ scope.where(field => value.first..value.last)
75
77
  when "starts_with"
76
- scope.where("#{field} LIKE ?", "#{value}%")
78
+ scope.where(arel_table_for(scope, field).matches("#{value}%"))
77
79
  when "ends_with"
78
- scope.where("#{field} LIKE ?", "%#{value}")
80
+ scope.where(arel_table_for(scope, field).matches("%#{value}"))
79
81
  when "contains"
80
- scope.where("#{field} LIKE ?", "%#{value}%")
82
+ scope.where(arel_table_for(scope, field).matches("%#{value}%"))
81
83
  when "is_null"
82
84
  scope.where(field => nil)
83
85
  when "is_not_null"
84
86
  scope.where.not(field => nil)
85
- else # '>', '>=', '<', '<='
86
- scope.where("#{field} #{relation} ?", value)
87
+ when "gt"
88
+ scope.where.not(field => ..value)
89
+ when "gte"
90
+ scope.where(field => value..)
91
+ when "lt"
92
+ scope.where.not(field => value..)
93
+ when "lte"
94
+ scope.where(field => ..value)
87
95
  end
88
96
  end
97
+
98
+ # @param scope [ActiveRecord::Relation]
99
+ # @param field [String|Symbol]
100
+ # @return [Arel::Table]
101
+ # The arel table for the given field. If the field has the format "table.column",
102
+ # the method will return the table for the field's table and column.
103
+ def arel_table_for(scope, field)
104
+ column, relation = field.to_s.split(".").reverse
105
+ return scope.arel_table[column.to_sym] unless relation
106
+
107
+ scope.arel_table.alias(relation.to_sym)[column.to_sym]
108
+ end
89
109
  end
90
110
  end
91
111
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/delegation"
4
+
5
+ module Warped
6
+ module Table
7
+ class Action
8
+ attr_reader :options
9
+
10
+ delegate :[], to: :options
11
+
12
+ # @param name [String | Symbol | Proc] The name of the action
13
+ # @param path [String | Symbol | Proc] The path of the action
14
+ def initialize(name, path, **options)
15
+ @name = name
16
+ @path = path
17
+ @options = options
18
+ end
19
+
20
+ def path(...)
21
+ return @path.call(...) if @path.is_a?(Proc)
22
+
23
+ @path
24
+ end
25
+
26
+ def name(...)
27
+ return @name.call(...) if @name.is_a?(Proc)
28
+
29
+ @name
30
+ end
31
+ end
32
+ end
33
+ end