cafe_car 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/README.md +155 -40
- data/Rakefile +9 -1
- data/app/assets/fonts/Lexend.css +7 -0
- data/app/assets/fonts/Lexend.ttf +0 -0
- data/app/assets/stylesheets/cafe_car/themes/defaults.css +58 -59
- data/app/assets/stylesheets/cafe_car/tooltips.css +20 -0
- data/app/assets/stylesheets/cafe_car/utility.css +1 -2
- data/app/assets/stylesheets/cafe_car.css +17 -6
- data/app/assets/stylesheets/ui/Alert.css +2 -1
- data/app/assets/stylesheets/ui/Button.css +6 -6
- data/app/assets/stylesheets/ui/Card.css +7 -3
- data/app/assets/stylesheets/ui/Chat.css +33 -0
- data/app/assets/stylesheets/ui/Close.css +11 -0
- data/app/assets/stylesheets/ui/Grid.css +2 -0
- data/app/assets/stylesheets/ui/Icon.css +3 -3
- data/app/assets/stylesheets/ui/Layout.css +20 -13
- data/app/assets/stylesheets/ui/Modal.css +5 -12
- data/app/assets/stylesheets/ui/Navigation.css +13 -5
- data/app/assets/stylesheets/ui/Page.css +42 -3
- data/app/assets/stylesheets/ui/Table.css +27 -56
- data/app/assets/stylesheets/ui/components.css +2 -0
- data/app/controllers/cafe_car/examples_controller.rb +1 -1
- data/app/controllers/cafe_car/sessions_controller.rb +30 -0
- data/app/controllers/concerns/cafe_car/authentication.rb +61 -0
- data/app/javascript/cafe_car.js +16 -11
- data/app/models/cafe_car/session.rb +18 -0
- data/app/policies/cafe_car/session_policy.rb +19 -0
- data/app/presenters/cafe_car/active_storage/attachment_presenter.rb +5 -4
- data/app/presenters/cafe_car/code_presenter.rb +18 -0
- data/app/presenters/cafe_car/date_presenter.rb +1 -0
- data/app/presenters/cafe_car/date_time_presenter.rb +2 -2
- data/app/presenters/cafe_car/enumerable_presenter.rb +1 -1
- data/app/presenters/cafe_car/hash_presenter.rb +3 -8
- data/app/presenters/cafe_car/presenter.rb +22 -12
- data/app/presenters/cafe_car/string_presenter.rb +2 -2
- data/app/ui/cafe_car/ui/button.rb +2 -1
- data/app/ui/cafe_car/ui/card.rb +18 -0
- data/app/ui/cafe_car/ui/grid.rb +30 -0
- data/app/ui/cafe_car/ui/layout.rb +7 -0
- data/app/ui/cafe_car/ui/page.rb +5 -1
- data/app/views/application/_body.html.haml +2 -1
- data/app/views/application/_controls.html.haml +1 -0
- data/app/views/application/_debug.html.haml +9 -2
- data/app/views/application/_errors.html.haml +4 -8
- data/app/views/application/_grid.html.haml +1 -1
- data/app/views/application/_grid_item.html.haml +1 -1
- data/app/views/application/_head.html.haml +1 -0
- data/app/views/application/_index.html.haml +6 -2
- data/app/views/application/_index_actions.html.haml +3 -3
- data/app/views/application/_navigation.html.haml +7 -0
- data/app/views/application/_navigation_links.html.haml +1 -1
- data/app/views/application/_notes.html.haml +1 -0
- data/app/views/application/_popup.html.haml +7 -0
- data/app/views/cafe_car/application/edit.html.haml +1 -1
- data/app/views/cafe_car/application/edit.turbo_stream.haml +3 -5
- data/app/views/cafe_car/application/index.html.haml +3 -0
- data/app/views/cafe_car/application/new.turbo_stream.haml +5 -6
- data/app/views/cafe_car/application/show.html.haml +2 -2
- data/app/views/cafe_car/examples/ui/_chat.html.haml +3 -0
- data/app/views/cafe_car/examples/ui/_info_circle.html.haml +1 -1
- data/app/views/cafe_car/examples/ui/_modal.html.haml +1 -1
- data/app/views/passwords_mailer/reset.html.haml +5 -0
- data/app/views/passwords_mailer/reset.text.erb +4 -0
- data/app/views/ui/_card.html.haml +6 -11
- data/app/views/ui/_field.html.haml +1 -7
- data/app/views/ui/_modal_close.html.haml +1 -2
- data/app/views/ui/_page.html.haml +6 -12
- data/config/brakeman.ignore +3 -3
- data/config/locales/en.yml +10 -2
- data/config/routes.rb +5 -1
- data/db/migrate/20251005220017_create_slugs.rb +2 -2
- data/lib/cafe_car/active_record.rb +1 -1
- data/lib/cafe_car/application_responder.rb +6 -0
- data/lib/cafe_car/attributes.rb +1 -1
- data/lib/cafe_car/auto_resolver.rb +1 -1
- data/lib/cafe_car/component.rb +102 -39
- data/lib/cafe_car/context.rb +5 -4
- data/lib/cafe_car/controller/filtering.rb +9 -1
- data/lib/cafe_car/controller.rb +52 -13
- data/lib/cafe_car/core_ext/array.rb +13 -0
- data/lib/cafe_car/core_ext/hash.rb +15 -0
- data/lib/cafe_car/core_ext/module.rb +15 -0
- data/lib/cafe_car/core_ext.rb +0 -2
- data/lib/cafe_car/current.rb +4 -1
- data/lib/cafe_car/engine.rb +9 -2
- data/lib/cafe_car/field_builder.rb +1 -1
- data/lib/cafe_car/field_info.rb +14 -12
- data/lib/cafe_car/fields.rb +7 -0
- data/lib/cafe_car/filter/field_info.rb +1 -1
- data/lib/cafe_car/filter/form_builder.rb +2 -2
- data/lib/cafe_car/filter_builder.rb +1 -1
- data/lib/cafe_car/form_builder.rb +1 -1
- data/lib/cafe_car/generators.rb +1 -1
- data/lib/cafe_car/helpers.rb +37 -10
- data/lib/cafe_car/href_builder.rb +35 -9
- data/lib/cafe_car/input_builder.rb +1 -1
- data/lib/cafe_car/link_builder.rb +14 -11
- data/lib/cafe_car/model.rb +2 -2
- data/lib/cafe_car/navigation.rb +10 -10
- data/lib/cafe_car/option_helpers.rb +11 -5
- data/lib/cafe_car/param_parser.rb +10 -6
- data/lib/cafe_car/policy.rb +2 -2
- data/lib/cafe_car/query_builder.rb +3 -3
- data/lib/cafe_car/resolver.rb +5 -1
- data/lib/cafe_car/routing.rb +1 -1
- data/lib/cafe_car/table/builder.rb +3 -2
- data/lib/cafe_car/table/head_builder.rb +2 -2
- data/lib/cafe_car/table/label_builder.rb +1 -1
- data/lib/cafe_car/table/row_builder.rb +5 -7
- data/lib/cafe_car/table_builder.rb +3 -3
- data/lib/cafe_car/ui.rb +2 -0
- data/lib/cafe_car/version.rb +1 -1
- data/lib/cafe_car/visitors.rb +2 -2
- data/lib/cafe_car.rb +25 -0
- data/lib/generators/cafe_car/controller/templates/controller.rb.tt +1 -1
- data/lib/generators/cafe_car/install/install_generator.rb +0 -1
- data/lib/generators/cafe_car/sessions/USAGE +17 -0
- data/lib/generators/cafe_car/sessions/sessions_generator.rb +29 -0
- data/lib/generators/cafe_car/sessions/templates/create_sessions.rb.tt +12 -0
- data/lib/tasks/holdco_tasks.rake +532 -0
- data/lib/tasks/templates/tasks_header.md +37 -0
- metadata +76 -48
- data/app/models/cafe_car/slug.rb +0 -3
- data/app/views/ui/_grid.html.haml +0 -17
- data/app/views/ui/_layout_menu.html.haml +0 -2
|
@@ -36,7 +36,7 @@ module CafeCar
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def label(method, text = info(method).label, required: info(method).required?, **, &)
|
|
39
|
-
super(method, @template.safe_join([text, required ? "*" : ""]), required:, **, &)
|
|
39
|
+
super(method, @template.safe_join([ text, required ? "*" : "" ]), required:, **, &)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def submit(value = nil, **options)
|
data/lib/cafe_car/generators.rb
CHANGED
|
@@ -15,7 +15,7 @@ module CafeCar::Generators
|
|
|
15
15
|
template("#{name}.rb", "app/models/#{name}.rb", ...)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def class_namespace = class_path.join(
|
|
18
|
+
def class_namespace = class_path.join("/").classify
|
|
19
19
|
|
|
20
20
|
def module_namespacing(&block)
|
|
21
21
|
super { concat wrap_with_module(class_namespace, &block) }
|
data/lib/cafe_car/helpers.rb
CHANGED
|
@@ -17,20 +17,21 @@ module CafeCar
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def ui_class(names, *args, **opts)
|
|
20
|
-
names = [*names].map(&:
|
|
20
|
+
names = [ *names ].map(&:camelize)
|
|
21
21
|
name = names.join("_")
|
|
22
|
-
parent = names.first
|
|
23
22
|
args.flatten!
|
|
24
23
|
args.compact_blank!
|
|
25
24
|
opts.compact_blank!
|
|
25
|
+
opts.merge!(*args.extract!(Hash))
|
|
26
26
|
|
|
27
|
-
flags = args.extract!
|
|
28
|
-
flags.
|
|
27
|
+
flags = args.extract!(Symbol)
|
|
28
|
+
flags |= opts.extract_if! { _1.is_a? Symbol }.keys
|
|
29
|
+
flags.map! { "#{name}-#{_1}" }
|
|
29
30
|
|
|
30
|
-
[*name, *flags, *args, *opts.keys].join(" ")
|
|
31
|
+
[ *name, *flags, *args, *opts.keys ].join(" ")
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
def body_classes = [*controller_path.split(?/), action_name, *@body_class]
|
|
34
|
+
def body_classes = [ *controller_path.split(?/), action_name, *@body_class ]
|
|
34
35
|
|
|
35
36
|
def title(object)
|
|
36
37
|
present(object).title.presence.tap do |title|
|
|
@@ -38,6 +39,18 @@ module CafeCar
|
|
|
38
39
|
end
|
|
39
40
|
end
|
|
40
41
|
|
|
42
|
+
def cat(*args)
|
|
43
|
+
args.flatten.each do |arg|
|
|
44
|
+
arg = capture(&arg) if arg.respond_to?(:to_proc)
|
|
45
|
+
arg = arg.to_s
|
|
46
|
+
concat(arg) if arg.present?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cap(*)
|
|
51
|
+
capture { cat(*) }
|
|
52
|
+
end
|
|
53
|
+
|
|
41
54
|
def capture(*, &)
|
|
42
55
|
super do
|
|
43
56
|
yield(*).then { _1.try(:html_safe?) ? _1.to_s : _1 }
|
|
@@ -46,17 +59,24 @@ module CafeCar
|
|
|
46
59
|
|
|
47
60
|
def present(*args, **options)
|
|
48
61
|
@presenters ||= {}
|
|
49
|
-
@presenters[[args, options]] ||= CafeCar[:Presenter].present(self, *args, **options)
|
|
62
|
+
@presenters[[ args, options ]] ||= CafeCar[:Presenter].present(self, *args, **options)
|
|
50
63
|
end
|
|
51
64
|
alias_method :p, :present
|
|
52
65
|
|
|
53
66
|
def current_href?(*, check_parameters: false, **) = current_page?(href_for(*, **), check_parameters:)
|
|
54
|
-
def ancestor_href?(...) = URI(href_for(...)) < URI(url_for(request.
|
|
67
|
+
def ancestor_href?(...) = URI(href_for(...)) < URI(url_for(request.url))
|
|
55
68
|
|
|
56
69
|
def href_for(*parts, namespace: self.namespace, **params)
|
|
57
70
|
HrefBuilder.new(*parts, namespace:, template: self, **params).to_s
|
|
58
71
|
end
|
|
59
72
|
|
|
73
|
+
def view_url(view)
|
|
74
|
+
view = view.to_s
|
|
75
|
+
params = request.params.merge(view:)
|
|
76
|
+
params.delete(:view) if params[:view] == default_view
|
|
77
|
+
url_for(params)
|
|
78
|
+
end
|
|
79
|
+
|
|
60
80
|
def context(name = nil, &)
|
|
61
81
|
@context ||= []
|
|
62
82
|
|
|
@@ -83,7 +103,10 @@ module CafeCar
|
|
|
83
103
|
@links[object] ||= CafeCar[:LinkBuilder].new(self, object)
|
|
84
104
|
end
|
|
85
105
|
|
|
86
|
-
def link_to(...)
|
|
106
|
+
def link_to(...)
|
|
107
|
+
raise ArgumentError, "Links cannot be nested" if context?(:a)
|
|
108
|
+
context(:a) { super }
|
|
109
|
+
end
|
|
87
110
|
|
|
88
111
|
def icon(name = nil, *, **, &)
|
|
89
112
|
case name
|
|
@@ -97,7 +120,7 @@ module CafeCar
|
|
|
97
120
|
end
|
|
98
121
|
|
|
99
122
|
def breadcrumbs(*items)
|
|
100
|
-
ui.Row safe_join(items, icon(:nav_arrow_right, :dim))
|
|
123
|
+
ui.Row safe_join(items.compact_blank, icon(:nav_arrow_right, :dim))
|
|
101
124
|
end
|
|
102
125
|
|
|
103
126
|
def filter_form_for(objects, **options, &block)
|
|
@@ -119,6 +142,10 @@ module CafeCar
|
|
|
119
142
|
def debug? = params.key?(:debug)
|
|
120
143
|
def console? = params.key?(:console)
|
|
121
144
|
|
|
145
|
+
def comment(text)
|
|
146
|
+
"<!-- #{text} -->".html_safe
|
|
147
|
+
end
|
|
148
|
+
|
|
122
149
|
def partial?(path)
|
|
123
150
|
prefixes = path.include?(?/) ? [] : lookup_context.prefixes
|
|
124
151
|
lookup_context.any?(path, prefixes, true)
|
|
@@ -11,11 +11,21 @@ module CafeCar
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def to_s
|
|
14
|
-
case [*@parts]
|
|
15
|
-
in [String]
|
|
16
|
-
[*@parts, *@params.to_query.presence].join(??)
|
|
14
|
+
case [ *@parts ]
|
|
15
|
+
in [ String ]
|
|
16
|
+
[ *@parts, *@params.to_query.presence ].join(??)
|
|
17
|
+
in []
|
|
18
|
+
@template.url_for(@params)
|
|
17
19
|
else
|
|
18
|
-
|
|
20
|
+
namespace = collapsed_namespace
|
|
21
|
+
parts = @parts.map { singular_resource(_1) }
|
|
22
|
+
begin
|
|
23
|
+
@template.url_for([ *namespace, *parts, @params ])
|
|
24
|
+
rescue NoMethodError
|
|
25
|
+
raise if namespace.empty?
|
|
26
|
+
namespace.pop
|
|
27
|
+
retry
|
|
28
|
+
end
|
|
19
29
|
end
|
|
20
30
|
end
|
|
21
31
|
|
|
@@ -41,13 +51,29 @@ module CafeCar
|
|
|
41
51
|
|
|
42
52
|
private
|
|
43
53
|
|
|
54
|
+
# Records route polymorphically via the plural `route_key`. For a singular
|
|
55
|
+
# resource (`resource :session`) that helper doesn't exist, so fall back to
|
|
56
|
+
# the singular route key. Leaves non-records (symbols, strings) untouched.
|
|
57
|
+
def singular_resource(part)
|
|
58
|
+
name = model_name_for(part) or return part
|
|
59
|
+
return part if @template.respond_to?("#{name.route_key}_path")
|
|
60
|
+
|
|
61
|
+
@template.respond_to?("#{name.singular_route_key}_path") ?
|
|
62
|
+
name.singular_route_key.to_sym : part
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def model_name_for(part)
|
|
66
|
+
klass = part.is_a?(Module) ? part : part.class
|
|
67
|
+
klass.model_name if klass.respond_to?(:model_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
44
70
|
def expand_part(part)
|
|
45
71
|
normalize case part
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
when Symbol, String, Hash, Array then part
|
|
73
|
+
when ActiveModel::Naming then part.model_name.collection
|
|
74
|
+
when Class then part.name.underscore
|
|
75
|
+
else expand_part(part.class)
|
|
76
|
+
end
|
|
51
77
|
end
|
|
52
78
|
|
|
53
79
|
def normalize(part)
|
|
@@ -18,7 +18,7 @@ module CafeCar
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def placeholder
|
|
21
|
-
I18n.t(method, scope: [:helpers, :placeholder, object_name], raise: true).presence
|
|
21
|
+
I18n.t(method, scope: [ :helpers, :placeholder, object_name ], raise: true).presence
|
|
22
22
|
rescue I18n::MissingTranslationData => _
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -15,37 +15,40 @@ module CafeCar
|
|
|
15
15
|
def can?(action) = policy.public_send("#{action}?")
|
|
16
16
|
def cant?(action) = !can?(action) && disabled(action, :policy)
|
|
17
17
|
|
|
18
|
-
def i18n(*, scope: nil, **) = p.i18n(*, scope: [:controls, *scope], **)
|
|
18
|
+
def i18n(*, scope: nil, **) = p.i18n(*, scope: [ :controls, *scope ], **)
|
|
19
19
|
|
|
20
20
|
def confirm(key) = i18n(key, scope: :confirm)
|
|
21
|
-
def disabled(action, reason) = i18n(action, scope: [:disabled, reason])
|
|
21
|
+
def disabled(action, reason) = i18n(action, scope: [ :disabled, reason ])
|
|
22
22
|
|
|
23
23
|
def turbo!(opts)
|
|
24
24
|
opts.replace({
|
|
25
|
-
data: {
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
data: {
|
|
26
|
+
turbo_stream: true,
|
|
27
|
+
turbo_method: opts.delete(:method),
|
|
28
|
+
turbo_confirm: opts.delete(:confirm)
|
|
28
29
|
}
|
|
29
30
|
}.deep_merge(opts))
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
def link(action, target, label = i18n(action), disabled: false, hide: false, **opts, &)
|
|
33
|
+
def link(action, target, label = i18n(action), disabled: false, hide: false, params: nil, **opts, &)
|
|
34
|
+
params ||= {}
|
|
33
35
|
disabled ||= cant?(action)
|
|
34
36
|
return if disabled and hide
|
|
35
37
|
|
|
36
|
-
href = href_for(*target, action:, namespace: @namespace)
|
|
37
|
-
current = current_page?(href)
|
|
38
|
+
href = href_for(*target, action:, namespace: @namespace, **params)
|
|
39
|
+
current = current_page?(href, check_parameters: true)
|
|
40
|
+
in_link = @template.context?(:a)
|
|
38
41
|
content = block_given? ? capture(label, &) : label
|
|
39
42
|
|
|
40
|
-
link_to_unless(disabled || current, content, href, **turbo!(opts)) do
|
|
43
|
+
link_to_unless(disabled || current || in_link, content, href, **turbo!(opts)) do
|
|
41
44
|
@template.tag.span(content, class: "disabled", disabled: true, title: disabled.presence)
|
|
42
45
|
end
|
|
43
46
|
end
|
|
44
47
|
|
|
45
|
-
def show(
|
|
48
|
+
def show(*, **, &) = link(:show, @object, *, data: { turbo_stream: nil }, **, &)
|
|
46
49
|
def edit(...) = link(:edit, @object, ...)
|
|
47
50
|
def destroy(*, **, &) = link(:destroy, @object, *, method: :delete, confirm: confirm(:destroy), **, &)
|
|
48
|
-
def index(*, **, &) = link(:index, model, *, hide: true, **, &)
|
|
51
|
+
def index(*, **, &) = link(:index, model, *, hide: true, data: { turbo_stream: nil }, **, &)
|
|
49
52
|
def new(*, **, &) = link(:new, model, *, hide: true, **, &)
|
|
50
53
|
|
|
51
54
|
def code(path = nil)
|
data/lib/cafe_car/model.rb
CHANGED
|
@@ -14,8 +14,8 @@ module CafeCar::Model
|
|
|
14
14
|
def normalize_sort_key(key)
|
|
15
15
|
case key
|
|
16
16
|
when /,/
|
|
17
|
-
key.split(
|
|
18
|
-
when /^-(.+)$/ then {$1 => :desc}
|
|
17
|
+
key.split(",").map { normalize_sort_key _1 }
|
|
18
|
+
when /^-(.+)$/ then { $1 => :desc }
|
|
19
19
|
else key
|
|
20
20
|
end
|
|
21
21
|
end
|
data/lib/cafe_car/navigation.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module CafeCar
|
|
2
2
|
class Navigation
|
|
3
3
|
class Route
|
|
4
|
-
delegate :tag, :ui_class, :capture, :concat, :t, to: :@template
|
|
4
|
+
delegate :tag, :ui, :ui_class, :capture, :concat, :t, to: :@template
|
|
5
5
|
delegate :name, :requirements, to: :@route
|
|
6
6
|
|
|
7
7
|
def initialize(route, template:)
|
|
@@ -30,17 +30,17 @@ module CafeCar
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def link(**opts)
|
|
33
|
-
@template.
|
|
33
|
+
ui.Navigation().Link(href: @template.href_for([ params ]), **opts) { content }
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
delegate :ui, :ui_class, to: :@template
|
|
38
|
-
|
|
39
37
|
def initialize(template, **options)
|
|
40
38
|
@template = template
|
|
41
39
|
@options = options
|
|
42
40
|
end
|
|
43
41
|
|
|
42
|
+
delegate :ui_class, to: :@template
|
|
43
|
+
|
|
44
44
|
def router = Rails.application.routes.router
|
|
45
45
|
def named_routes = Rails.application.routes.named_routes.to_h.values.map { Route.new(_1, template: @template) }
|
|
46
46
|
def index_routes = named_routes.select(&:index?)
|
|
@@ -53,13 +53,13 @@ module CafeCar
|
|
|
53
53
|
|
|
54
54
|
def recognize(obj, **)
|
|
55
55
|
req = case obj
|
|
56
|
-
|
|
56
|
+
when String
|
|
57
57
|
path = ActionDispatch::Journey::Router::Utils.normalize_path(path)
|
|
58
58
|
env = Rack::MockRequest.env_for(path, method: :get, **)
|
|
59
59
|
ActionDispatch::Request.new(env)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
when ActionDispatch::Request then obj
|
|
61
|
+
else raise "cannot recognize this obj"
|
|
62
|
+
end
|
|
63
63
|
|
|
64
64
|
router.recognize(req) do |route, params|
|
|
65
65
|
return Route.new(route, template: @template)
|
|
@@ -69,8 +69,8 @@ module CafeCar
|
|
|
69
69
|
def current = recognize(@template.request)
|
|
70
70
|
|
|
71
71
|
def link_to(*args, **opts, &block)
|
|
72
|
-
block ||= -> { @template.tag.span(_1, class: ui_class([:navigation, :link], :current)) }
|
|
73
|
-
@template.link_to_unless_current(*args, class: ui_class([:navigation, :link]), **opts, &block)
|
|
72
|
+
block ||= -> { @template.tag.span(_1, class: ui_class([ :navigation, :link ], :current)) }
|
|
73
|
+
@template.link_to_unless_current(*args, class: ui_class([ :navigation, :link ]), **opts, &block)
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
end
|
|
@@ -6,13 +6,15 @@ module CafeCar
|
|
|
6
6
|
class_attribute :option_defaults, default: {}
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
def get_options = @options
|
|
10
|
+
|
|
9
11
|
def assign_options!
|
|
10
|
-
raise "@options is not a hash" unless
|
|
12
|
+
raise "@options is not a hash" unless get_options.is_a? Hash
|
|
11
13
|
option_defaults.each { assign_option!(_1, _2) }
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def assign_option!(k, default)
|
|
15
|
-
value =
|
|
17
|
+
value = get_options.delete(k) { default.respond_to?(:call) ? default.call : default.clone }
|
|
16
18
|
instance_variable_set("@#{k}", value)
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -22,12 +24,16 @@ module CafeCar
|
|
|
22
24
|
subclass.option_defaults = option_defaults.deep_dup
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
def option(*names, default: nil, accessor: true, reader: accessor, writer: accessor, presence: accessor)
|
|
27
|
+
def option(*names, default: nil, accessor: true, reader: accessor, writer: accessor, presence: accessor, macro: accessor)
|
|
26
28
|
names.each do |name|
|
|
29
|
+
if respond_to?(name) and reader
|
|
30
|
+
raise ArgumentError, "Option name #{name} conflicts with existing method"
|
|
31
|
+
end
|
|
27
32
|
attr_reader name if reader
|
|
28
33
|
attr_writer name if writer
|
|
29
34
|
option_defaults[name] = default
|
|
30
35
|
define_method("#{name}?") { instance_variable_get("@#{name}").present? } if presence
|
|
36
|
+
define_singleton_method(name) { |v| option_defaults[name] = v } if macro
|
|
31
37
|
end
|
|
32
38
|
end
|
|
33
39
|
|
|
@@ -39,8 +45,8 @@ module CafeCar
|
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def option_accessor(name)
|
|
42
|
-
define_method(name) {
|
|
43
|
-
define_method("#{name}=") {
|
|
48
|
+
define_method(name) { get_options[name] }
|
|
49
|
+
define_method("#{name}=") { get_options[name] = _1 }
|
|
44
50
|
end
|
|
45
51
|
end
|
|
46
52
|
end
|
|
@@ -9,14 +9,14 @@ class CafeCar::ParamParser
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def params(params)
|
|
12
|
-
params.map {|k, v| k.split(
|
|
12
|
+
params.map { |k, v| k.split(".").reverse.reduce(value(v)) { { _2 => _1 } } }
|
|
13
13
|
.reduce({}) { _1.deep_merge(_2, &method(:merge)) }
|
|
14
14
|
.with_indifferent_access
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def merge(_, a, b)
|
|
18
18
|
if a.is_a?(Array) || b.is_a?(Array)
|
|
19
|
-
[*Array.wrap(a), *Array.wrap(b)]
|
|
19
|
+
[ *Array.wrap(a), *Array.wrap(b) ]
|
|
20
20
|
else
|
|
21
21
|
b
|
|
22
22
|
end
|
|
@@ -26,12 +26,16 @@ class CafeCar::ParamParser
|
|
|
26
26
|
case v
|
|
27
27
|
when Array then v.map { value(_1) }
|
|
28
28
|
when Hash then params(v).tap { _1.merge!(_1.delete("")) if _1[""] }
|
|
29
|
-
when '""', "''" then
|
|
30
|
-
when
|
|
29
|
+
when '""', "''" then ""
|
|
30
|
+
when "nil", "" then nil
|
|
31
31
|
when /[{}\[\]]/ then value(JSON.parse(v))
|
|
32
|
-
when /,/ then value(v.split(
|
|
32
|
+
when /,/ then value(v.split(","))
|
|
33
33
|
when /^(.*?)\.\.(\.?)(.*)$/
|
|
34
|
-
|
|
34
|
+
begin
|
|
35
|
+
Range.new(value($1), value($3), $2.present?)
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
v
|
|
38
|
+
end
|
|
35
39
|
when /^\$(\w+)\.(\w+)$/
|
|
36
40
|
# TODO: make less scary
|
|
37
41
|
$1.constantize.arel_table[$2]
|
data/lib/cafe_car/policy.rb
CHANGED
|
@@ -41,13 +41,13 @@ module CafeCar::Policy
|
|
|
41
41
|
|
|
42
42
|
def displayable_associations
|
|
43
43
|
model.reflections.values
|
|
44
|
-
.select {|a| !a.options[:autosave] && !a.options[:polymorphic] }
|
|
44
|
+
.select { |a| !a.options[:autosave] && !a.options[:polymorphic] }
|
|
45
45
|
.reject { _1.class_name =~ /^ActiveStorage::/ }
|
|
46
46
|
.map { _1.name.to_sym }
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def permitted_attribute_keys
|
|
50
|
-
permitted_attributes.flat_map {|a| a.try(:keys) || a }
|
|
50
|
+
permitted_attributes.flat_map { |a| a.try(:keys) || a }
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def permitted_association?(name)
|
|
@@ -32,7 +32,7 @@ module CafeCar
|
|
|
32
32
|
|
|
33
33
|
def unscoped = QueryBuilder.new(@scope.unscope(:where))
|
|
34
34
|
def arel(key) = @scope.arel_table[chomp(key)]
|
|
35
|
-
def chomp(key) = key.to_s.sub(/\W+$/,
|
|
35
|
+
def chomp(key) = key.to_s.sub(/\W+$/, "")
|
|
36
36
|
|
|
37
37
|
def parse_time(value)
|
|
38
38
|
Chronic.parse(value, guess: false, context: :past)
|
|
@@ -116,12 +116,12 @@ module CafeCar
|
|
|
116
116
|
when method(:scope?)
|
|
117
117
|
scope!(key, value)
|
|
118
118
|
else
|
|
119
|
-
raise "can't find #{key.inspect} on #{@scope.model_name}"
|
|
119
|
+
raise MissingAttributeError, "can't find #{key.inspect} on #{@scope.model_name}"
|
|
120
120
|
end
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
def attribute!(key, value)
|
|
124
|
-
case [key, value]
|
|
124
|
+
case [ key, value ]
|
|
125
125
|
in _, Regexp
|
|
126
126
|
@scope.where!(arel(key).matches_regexp(value.source, !value.casefold?))
|
|
127
127
|
in _, Op
|
data/lib/cafe_car/resolver.rb
CHANGED
|
@@ -6,10 +6,14 @@ module CafeCar
|
|
|
6
6
|
def const!(name) = self.class.const!(name)
|
|
7
7
|
|
|
8
8
|
class_methods do
|
|
9
|
-
def const_scopes = [self, *module_parents]
|
|
9
|
+
def const_scopes = [ self, *module_parents ]
|
|
10
10
|
|
|
11
11
|
def [](const_name) = const(const_name)
|
|
12
12
|
|
|
13
|
+
def const_try(name)
|
|
14
|
+
const_get(name) if const_defined?(name)
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
def const(name)
|
|
14
18
|
const_scopes.detect { _1.const_defined?(name) }
|
|
15
19
|
&.then { _1.const_get(name) }
|
data/lib/cafe_car/routing.rb
CHANGED
|
@@ -24,7 +24,7 @@ module CafeCar::Table
|
|
|
24
24
|
def has?(method) = model.info.fields.has?(method)
|
|
25
25
|
|
|
26
26
|
def title(method = policy.title_attribute, *, **, &)
|
|
27
|
-
cell(method, *, href: true, blank:
|
|
27
|
+
cell(method, *, href: true, blank: "(none)", **, &)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def logo(method = policy.logo_attribute, *, **, &)
|
|
@@ -39,7 +39,7 @@ module CafeCar::Table
|
|
|
39
39
|
|
|
40
40
|
def remaining(except: [])
|
|
41
41
|
capture do
|
|
42
|
-
(remaining_attributes - [*except]).each do |attr|
|
|
42
|
+
(remaining_attributes - [ *except ]).each do |attr|
|
|
43
43
|
ui << cell(policy.info(attr).displayable.method)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -47,5 +47,6 @@ module CafeCar::Table
|
|
|
47
47
|
|
|
48
48
|
def html_safe? = true
|
|
49
49
|
def to_s = @to_s ||= to_html.to_s
|
|
50
|
+
def ~@ = @template.concat(to_s)
|
|
50
51
|
end
|
|
51
52
|
end
|
|
@@ -10,7 +10,7 @@ module CafeCar::Table
|
|
|
10
10
|
def cell(method, *flags, label: label(method), **, &)
|
|
11
11
|
super
|
|
12
12
|
@fields << model.info.field(method)
|
|
13
|
-
ui.Cell(
|
|
13
|
+
ui.Cell(*flags) { label }
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def label(method)
|
|
@@ -21,6 +21,6 @@ module CafeCar::Table
|
|
|
21
21
|
|
|
22
22
|
def controls(*, **) = cell(:controls, :controls, *, label: nil, **)
|
|
23
23
|
|
|
24
|
-
def to_html = ui.Head(:sticky
|
|
24
|
+
def to_html = ui.Head(:sticky) { capture(self, &@block) }
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -42,7 +42,7 @@ module CafeCar::Table
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def label = @method&.then { present(@objects).human(_1) }
|
|
45
|
-
def content = @template.safe_join([label, label_sort], " ".html_safe)
|
|
45
|
+
def content = @template.safe_join([ label, label_sort ], " ".html_safe)
|
|
46
46
|
def to_html = @template.link_to_unless(!sortable?, content, href)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -20,22 +20,20 @@ module CafeCar::Table
|
|
|
20
20
|
super
|
|
21
21
|
options[:href] = @object if options[:href] == true
|
|
22
22
|
call_procs!(options, @object)
|
|
23
|
-
ui.Cell(show(method, **options.slice(:blank), &)
|
|
23
|
+
ui.Cell(*flags, **options) { show(method, **options.slice(:blank), &) }
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def timestamps(**options)
|
|
27
|
-
|
|
28
|
-
cell(timestamp_attribute, :shrink,
|
|
27
|
+
tip = show(:created_at).string&.then { "Created: #{_1}" }
|
|
28
|
+
cell(timestamp_attribute, :shrink, tip:, **options)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def controls(*args, **options)
|
|
32
|
-
ui.Cell(:shrink, :shy, :controls, *args
|
|
32
|
+
ui.Cell(:shrink, :shy, :controls, *args) { present(@object).controls(*args, **options) }
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def content = capture(self, &@block)
|
|
36
|
-
|
|
37
35
|
def to_html
|
|
38
|
-
ui.Row(
|
|
36
|
+
ui.Row { safe_join [ capture(self, &@block), @template.turbo_stream_from(@object) ] }
|
|
39
37
|
end
|
|
40
38
|
end
|
|
41
39
|
end
|
|
@@ -4,9 +4,9 @@ module CafeCar
|
|
|
4
4
|
head = Table::HeadBuilder.new(@template, **@options, &@block).tap(&:to_s)
|
|
5
5
|
columns = head.fields.map(&:width).join(" ")
|
|
6
6
|
@template.ui.Table style: "grid-template-columns: #{columns}" do |table|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
~head
|
|
8
|
+
~Table::BodyBuilder.new(@template, **@options, &@block)
|
|
9
|
+
~Table::FootBuilder.new(@template, **@options, &@block)
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
end
|
data/lib/cafe_car/ui.rb
CHANGED
data/lib/cafe_car/version.rb
CHANGED
data/lib/cafe_car/visitors.rb
CHANGED
|
@@ -10,12 +10,12 @@ module CafeCar::Visitors
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def visit_Arel_Nodes_Regexp(o, collector)
|
|
13
|
-
function("regexp", [o.right, o.left, bool(o.case_sensitive)], collector)
|
|
13
|
+
function("regexp", [ o.right, o.left, bool(o.case_sensitive) ], collector)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def visit_Arel_Nodes_NotRegexp(o, collector)
|
|
17
17
|
collector << "NOT "
|
|
18
|
-
function("regexp", [o.right, o.left, bool(o.case_sensitive)], collector)
|
|
18
|
+
function("regexp", [ o.right, o.left, bool(o.case_sensitive) ], collector)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
data/lib/cafe_car.rb
CHANGED
|
@@ -10,4 +10,29 @@ module CafeCar
|
|
|
10
10
|
include Resolver
|
|
11
11
|
extend AutoResolver
|
|
12
12
|
extend ProcHelpers
|
|
13
|
+
|
|
14
|
+
class MissingAttributeError < StandardError
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class AuthenticationFailed < StandardError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.use_relative_model_naming? = true
|
|
21
|
+
|
|
22
|
+
# Name of the host application's user model. Hosts with a differently named
|
|
23
|
+
# user model can override this (e.g. `CafeCar.user_class_name = "Account"`).
|
|
24
|
+
mattr_accessor :user_class_name, default: "User"
|
|
25
|
+
|
|
26
|
+
# The host's user model, resolved lazily so the constant need not exist at
|
|
27
|
+
# boot. Used by CafeCar::Session for authentication.
|
|
28
|
+
def self.user_class = user_class_name.to_s.constantize
|
|
29
|
+
|
|
30
|
+
# Whether the opt-in sessions/login infrastructure is available. True only
|
|
31
|
+
# when the sessions table exists, so a CRUD-only host (no sessions migration)
|
|
32
|
+
# degrades to 403 Forbidden instead of redirecting to a nonexistent login.
|
|
33
|
+
def self.sessions_available?
|
|
34
|
+
CafeCar[:Session].table_exists?
|
|
35
|
+
rescue StandardError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
13
38
|
end
|