warped 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +25 -0
- data/Gemfile +0 -2
- data/Gemfile.lock +25 -19
- data/README.md +116 -270
- data/app/assets/config/warped_manifest.js +2 -0
- data/app/assets/javascript/warped/controllers/filter_controller.js +76 -0
- data/app/assets/javascript/warped/controllers/filters_controller.js +21 -0
- data/app/assets/javascript/warped/index.js +2 -0
- data/app/assets/stylesheets/warped/application.css +15 -0
- data/app/assets/stylesheets/warped/base.css +23 -0
- data/app/assets/stylesheets/warped/filters.css +115 -0
- data/app/assets/stylesheets/warped/pagination.css +74 -0
- data/app/assets/stylesheets/warped/search.css +33 -0
- data/app/assets/stylesheets/warped/table.css +114 -0
- data/app/views/warped/_actions.html.erb +9 -0
- data/app/views/warped/_cell.html.erb +3 -0
- data/app/views/warped/_column.html.erb +35 -0
- data/app/views/warped/_filters.html.erb +21 -0
- data/app/views/warped/_hidden_fields.html.erb +19 -0
- data/app/views/warped/_pagination.html.erb +34 -0
- data/app/views/warped/_row.html.erb +19 -0
- data/app/views/warped/_search.html.erb +21 -0
- data/app/views/warped/_table.html.erb +52 -0
- data/app/views/warped/filters/_filter.html.erb +40 -0
- data/config/importmap.rb +3 -0
- data/docs/controllers/FILTERABLE.md +193 -0
- data/docs/controllers/PAGEABLE.md +70 -0
- data/docs/controllers/README.md +8 -0
- data/docs/controllers/SEARCHABLE.md +95 -0
- data/docs/controllers/SORTABLE.md +94 -0
- data/docs/controllers/TABULATABLE.md +28 -0
- data/docs/controllers/views/PARTIALS.md +285 -0
- data/docs/jobs/README.md +22 -0
- data/docs/services/README.md +81 -0
- data/lib/generators/warped/install_generator.rb +1 -1
- data/lib/warped/api/filter/base/value.rb +52 -0
- data/lib/warped/api/filter/base.rb +84 -0
- data/lib/warped/api/filter/boolean.rb +41 -0
- data/lib/warped/api/filter/date.rb +26 -0
- data/lib/warped/api/filter/date_time.rb +32 -0
- data/lib/warped/api/filter/decimal.rb +31 -0
- data/lib/warped/api/filter/factory.rb +38 -0
- data/lib/warped/api/filter/integer.rb +38 -0
- data/lib/warped/api/filter/string.rb +25 -0
- data/lib/warped/api/filter/time.rb +25 -0
- data/lib/warped/api/filter.rb +14 -0
- data/lib/warped/api/sort/value.rb +40 -0
- data/lib/warped/api/sort.rb +65 -0
- data/lib/warped/controllers/filterable/ui.rb +46 -0
- data/lib/warped/controllers/filterable.rb +79 -42
- data/lib/warped/controllers/pageable/ui.rb +70 -0
- data/lib/warped/controllers/pageable.rb +11 -11
- data/lib/warped/controllers/searchable/ui.rb +37 -0
- data/lib/warped/controllers/searchable.rb +2 -0
- data/lib/warped/controllers/sortable/ui.rb +53 -0
- data/lib/warped/controllers/sortable.rb +53 -33
- data/lib/warped/controllers/tabulatable/ui.rb +54 -0
- data/lib/warped/controllers/tabulatable.rb +13 -27
- data/lib/warped/emails/components/align.rb +21 -0
- data/lib/warped/emails/components/base.rb +116 -0
- data/lib/warped/emails/components/button.rb +58 -0
- data/lib/warped/emails/components/divider.rb +15 -0
- data/lib/warped/emails/components/heading.rb +65 -0
- data/lib/warped/emails/components/layouts/columns.rb +36 -0
- data/lib/warped/emails/components/layouts/cta.rb +38 -0
- data/lib/warped/emails/components/layouts/main.rb +34 -0
- data/lib/warped/emails/components/link.rb +36 -0
- data/lib/warped/emails/components/spacer.rb +15 -0
- data/lib/warped/emails/components/stepper.rb +104 -0
- data/lib/warped/emails/components/table.rb +37 -0
- data/lib/warped/emails/components/text.rb +67 -0
- data/lib/warped/emails/helpers.rb +26 -0
- data/lib/warped/emails/slottable.rb +61 -0
- data/lib/warped/emails/styleable.rb +160 -0
- data/lib/warped/engine.rb +19 -0
- data/lib/warped/queries/filter.rb +32 -12
- data/lib/warped/table/action.rb +33 -0
- data/lib/warped/table/column.rb +34 -0
- data/lib/warped/version.rb +1 -1
- data/lib/warped.rb +2 -0
- data/warped.gemspec +1 -1
- metadata +73 -7
- 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,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. "
|
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: "
|
21
|
-
# { relation: "
|
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 =
|
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 "
|
69
|
+
when "eq"
|
70
70
|
scope.where(field => value)
|
71
|
-
when "
|
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(
|
76
|
+
scope.where(field => value.first..value.last)
|
75
77
|
when "starts_with"
|
76
|
-
scope.where(
|
78
|
+
scope.where(arel_table_for(scope, field).matches("#{value}%"))
|
77
79
|
when "ends_with"
|
78
|
-
scope.where(
|
80
|
+
scope.where(arel_table_for(scope, field).matches("%#{value}"))
|
79
81
|
when "contains"
|
80
|
-
scope.where(
|
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
|
-
|
86
|
-
scope.where(
|
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
|