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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +155 -40
  3. data/Rakefile +9 -1
  4. data/app/assets/fonts/Lexend.css +7 -0
  5. data/app/assets/fonts/Lexend.ttf +0 -0
  6. data/app/assets/stylesheets/cafe_car/themes/defaults.css +58 -59
  7. data/app/assets/stylesheets/cafe_car/tooltips.css +20 -0
  8. data/app/assets/stylesheets/cafe_car/utility.css +1 -2
  9. data/app/assets/stylesheets/cafe_car.css +17 -6
  10. data/app/assets/stylesheets/ui/Alert.css +2 -1
  11. data/app/assets/stylesheets/ui/Button.css +6 -6
  12. data/app/assets/stylesheets/ui/Card.css +7 -3
  13. data/app/assets/stylesheets/ui/Chat.css +33 -0
  14. data/app/assets/stylesheets/ui/Close.css +11 -0
  15. data/app/assets/stylesheets/ui/Grid.css +2 -0
  16. data/app/assets/stylesheets/ui/Icon.css +3 -3
  17. data/app/assets/stylesheets/ui/Layout.css +20 -13
  18. data/app/assets/stylesheets/ui/Modal.css +5 -12
  19. data/app/assets/stylesheets/ui/Navigation.css +13 -5
  20. data/app/assets/stylesheets/ui/Page.css +42 -3
  21. data/app/assets/stylesheets/ui/Table.css +27 -56
  22. data/app/assets/stylesheets/ui/components.css +2 -0
  23. data/app/controllers/cafe_car/examples_controller.rb +1 -1
  24. data/app/controllers/cafe_car/sessions_controller.rb +30 -0
  25. data/app/controllers/concerns/cafe_car/authentication.rb +61 -0
  26. data/app/javascript/cafe_car.js +16 -11
  27. data/app/models/cafe_car/session.rb +18 -0
  28. data/app/policies/cafe_car/session_policy.rb +19 -0
  29. data/app/presenters/cafe_car/active_storage/attachment_presenter.rb +5 -4
  30. data/app/presenters/cafe_car/code_presenter.rb +18 -0
  31. data/app/presenters/cafe_car/date_presenter.rb +1 -0
  32. data/app/presenters/cafe_car/date_time_presenter.rb +2 -2
  33. data/app/presenters/cafe_car/enumerable_presenter.rb +1 -1
  34. data/app/presenters/cafe_car/hash_presenter.rb +3 -8
  35. data/app/presenters/cafe_car/presenter.rb +22 -12
  36. data/app/presenters/cafe_car/string_presenter.rb +2 -2
  37. data/app/ui/cafe_car/ui/button.rb +2 -1
  38. data/app/ui/cafe_car/ui/card.rb +18 -0
  39. data/app/ui/cafe_car/ui/grid.rb +30 -0
  40. data/app/ui/cafe_car/ui/layout.rb +7 -0
  41. data/app/ui/cafe_car/ui/page.rb +5 -1
  42. data/app/views/application/_body.html.haml +2 -1
  43. data/app/views/application/_controls.html.haml +1 -0
  44. data/app/views/application/_debug.html.haml +9 -2
  45. data/app/views/application/_errors.html.haml +4 -8
  46. data/app/views/application/_grid.html.haml +1 -1
  47. data/app/views/application/_grid_item.html.haml +1 -1
  48. data/app/views/application/_head.html.haml +1 -0
  49. data/app/views/application/_index.html.haml +6 -2
  50. data/app/views/application/_index_actions.html.haml +3 -3
  51. data/app/views/application/_navigation.html.haml +7 -0
  52. data/app/views/application/_navigation_links.html.haml +1 -1
  53. data/app/views/application/_notes.html.haml +1 -0
  54. data/app/views/application/_popup.html.haml +7 -0
  55. data/app/views/cafe_car/application/edit.html.haml +1 -1
  56. data/app/views/cafe_car/application/edit.turbo_stream.haml +3 -5
  57. data/app/views/cafe_car/application/index.html.haml +3 -0
  58. data/app/views/cafe_car/application/new.turbo_stream.haml +5 -6
  59. data/app/views/cafe_car/application/show.html.haml +2 -2
  60. data/app/views/cafe_car/examples/ui/_chat.html.haml +3 -0
  61. data/app/views/cafe_car/examples/ui/_info_circle.html.haml +1 -1
  62. data/app/views/cafe_car/examples/ui/_modal.html.haml +1 -1
  63. data/app/views/passwords_mailer/reset.html.haml +5 -0
  64. data/app/views/passwords_mailer/reset.text.erb +4 -0
  65. data/app/views/ui/_card.html.haml +6 -11
  66. data/app/views/ui/_field.html.haml +1 -7
  67. data/app/views/ui/_modal_close.html.haml +1 -2
  68. data/app/views/ui/_page.html.haml +6 -12
  69. data/config/brakeman.ignore +3 -3
  70. data/config/locales/en.yml +10 -2
  71. data/config/routes.rb +5 -1
  72. data/db/migrate/20251005220017_create_slugs.rb +2 -2
  73. data/lib/cafe_car/active_record.rb +1 -1
  74. data/lib/cafe_car/application_responder.rb +6 -0
  75. data/lib/cafe_car/attributes.rb +1 -1
  76. data/lib/cafe_car/auto_resolver.rb +1 -1
  77. data/lib/cafe_car/component.rb +102 -39
  78. data/lib/cafe_car/context.rb +5 -4
  79. data/lib/cafe_car/controller/filtering.rb +9 -1
  80. data/lib/cafe_car/controller.rb +52 -13
  81. data/lib/cafe_car/core_ext/array.rb +13 -0
  82. data/lib/cafe_car/core_ext/hash.rb +15 -0
  83. data/lib/cafe_car/core_ext/module.rb +15 -0
  84. data/lib/cafe_car/core_ext.rb +0 -2
  85. data/lib/cafe_car/current.rb +4 -1
  86. data/lib/cafe_car/engine.rb +9 -2
  87. data/lib/cafe_car/field_builder.rb +1 -1
  88. data/lib/cafe_car/field_info.rb +14 -12
  89. data/lib/cafe_car/fields.rb +7 -0
  90. data/lib/cafe_car/filter/field_info.rb +1 -1
  91. data/lib/cafe_car/filter/form_builder.rb +2 -2
  92. data/lib/cafe_car/filter_builder.rb +1 -1
  93. data/lib/cafe_car/form_builder.rb +1 -1
  94. data/lib/cafe_car/generators.rb +1 -1
  95. data/lib/cafe_car/helpers.rb +37 -10
  96. data/lib/cafe_car/href_builder.rb +35 -9
  97. data/lib/cafe_car/input_builder.rb +1 -1
  98. data/lib/cafe_car/link_builder.rb +14 -11
  99. data/lib/cafe_car/model.rb +2 -2
  100. data/lib/cafe_car/navigation.rb +10 -10
  101. data/lib/cafe_car/option_helpers.rb +11 -5
  102. data/lib/cafe_car/param_parser.rb +10 -6
  103. data/lib/cafe_car/policy.rb +2 -2
  104. data/lib/cafe_car/query_builder.rb +3 -3
  105. data/lib/cafe_car/resolver.rb +5 -1
  106. data/lib/cafe_car/routing.rb +1 -1
  107. data/lib/cafe_car/table/builder.rb +3 -2
  108. data/lib/cafe_car/table/head_builder.rb +2 -2
  109. data/lib/cafe_car/table/label_builder.rb +1 -1
  110. data/lib/cafe_car/table/row_builder.rb +5 -7
  111. data/lib/cafe_car/table_builder.rb +3 -3
  112. data/lib/cafe_car/ui.rb +2 -0
  113. data/lib/cafe_car/version.rb +1 -1
  114. data/lib/cafe_car/visitors.rb +2 -2
  115. data/lib/cafe_car.rb +25 -0
  116. data/lib/generators/cafe_car/controller/templates/controller.rb.tt +1 -1
  117. data/lib/generators/cafe_car/install/install_generator.rb +0 -1
  118. data/lib/generators/cafe_car/sessions/USAGE +17 -0
  119. data/lib/generators/cafe_car/sessions/sessions_generator.rb +29 -0
  120. data/lib/generators/cafe_car/sessions/templates/create_sessions.rb.tt +12 -0
  121. data/lib/tasks/holdco_tasks.rake +532 -0
  122. data/lib/tasks/templates/tasks_header.md +37 -0
  123. metadata +76 -48
  124. data/app/models/cafe_car/slug.rb +0 -3
  125. data/app/views/ui/_grid.html.haml +0 -17
  126. 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)
@@ -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('/').classify
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) }
@@ -17,20 +17,21 @@ module CafeCar
17
17
  end
18
18
 
19
19
  def ui_class(names, *args, **opts)
20
- names = [*names].map(&:to_s).map(&:camelize)
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! { _1.is_a? Symbol } | opts.extract! { _1.is_a? Symbol }.keys
28
- flags.map! { [*parent, _1].join("-") }
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.params))
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(...) = context(:a) { super }
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
- @template.url_for([*collapsed_namespace, *@parts, @params])
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
- when Symbol, String, Hash, Array then part
47
- when ActiveModel::Naming then part.model_name.collection
48
- when Class then part.name.underscore
49
- else expand_part(part.class)
50
- end
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: {turbo_stream: true,
26
- turbo_method: opts.delete(:method),
27
- turbo_confirm: opts.delete(:confirm)
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(...) = link(:show, @object, ...)
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)
@@ -14,8 +14,8 @@ module CafeCar::Model
14
14
  def normalize_sort_key(key)
15
15
  case key
16
16
  when /,/
17
- key.split(',').map { normalize_sort_key _1 }
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
@@ -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.link_to(content, params, **opts, class: [*opts[:class], ("current" if @template.current_page?(params))])
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
- when String
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
- when ActionDispatch::Request then obj
61
- else raise "cannot recognize this obj"
62
- end
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 @options
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 = @options.delete(k) { default.respond_to?(:call) ? default.call : default.clone }
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) { @options[name] }
43
- define_method("#{name}=") { @options[name] = _1 }
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('.').reverse.reduce(value(v)) { {_2 => _1} } }
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 'nil', '' then nil
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
- Range.new(value($1), value($3), $2.present?)
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]
@@ -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
@@ -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) }
@@ -11,7 +11,7 @@ module CafeCar
11
11
  end
12
12
  end
13
13
 
14
- super(*, **, concerns: [:batchable, *concerns], &)
14
+ super(*, **, concerns: [ :batchable, *concerns ], &)
15
15
  end
16
16
  end
17
17
  end
@@ -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: '(none)', **, &)
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(label, *flags)
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, capture(self, &@block))
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], "&nbsp;".html_safe)
45
+ def content = @template.safe_join([ label, label_sort ], "&nbsp;".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), &), *flags, **options)
23
+ ui.Cell(*flags, **options) { show(method, **options.slice(:blank), &) }
24
24
  end
25
25
 
26
26
  def timestamps(**options)
27
- title = show(:created_at).string&.then { "Created: #{_1}" }
28
- cell(timestamp_attribute, :shrink, title:, **options)
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, present(@object).controls(*args, **options))
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(content, @template.turbo_stream_from(@object))
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
- table << head
8
- table << Table::BodyBuilder.new(@template, **@options, &@block)
9
- table << Table::FootBuilder.new(@template, **@options, &@block)
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
@@ -1,5 +1,7 @@
1
1
  module CafeCar
2
2
  module UI
3
+ include Resolver
4
+
3
5
  module_function
4
6
 
5
7
  def component(name, **, &)
@@ -1,3 +1,3 @@
1
1
  module CafeCar
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  <% module_namespacing do -%>
2
2
  class <%= class_name %>Controller < <%= base_controller_name %>
3
- recline_in_the_cafe_car
3
+ cafe_car
4
4
  end
5
5
  <% end -%>
@@ -4,7 +4,6 @@ class CafeCar::InstallGenerator < Rails::Generators::Base
4
4
  source_root File.expand_path("templates", __dir__)
5
5
 
6
6
  def install_deps
7
- gem "cnc", github: "craft-concept/cnc"
8
7
  gem "bcrypt"
9
8
  gem "paper_trail"
10
9
  gem "factory_bot_rails"