warped 0.1.0 → 1.0.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 (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