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
data/config/routes.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  CafeCar::Engine.routes.draw do
2
2
  scope module: :cafe_car, as: :cafe_car do
3
- get 'components', to: "examples#index"
3
+ get "components", to: "examples#index"
4
4
  end
5
+
6
+ # Opt-in login/logout. Singular resource so the form posts to /session and
7
+ # request_authentication can redirect to new_session_path.
8
+ resource :session, only: %i[new create destroy], controller: "cafe_car/sessions"
5
9
  end
@@ -7,7 +7,7 @@ class CreateSlugs < ActiveRecord::Migration[8.0]
7
7
  t.datetime :created_at
8
8
  end
9
9
 
10
- add_index :slugs, [:slug, :sluggable_type]
11
- add_index :slugs, [:slug, :sluggable_type, :scope], unique: true
10
+ add_index :slugs, [ :slug, :sluggable_type ]
11
+ add_index :slugs, [ :slug, :sluggable_type, :scope ], unique: true
12
12
  end
13
13
  end
@@ -9,7 +9,7 @@ module CafeCar::ActiveRecord
9
9
  def initialize(*args)
10
10
  pre_cafe_car_initialize(*args)
11
11
 
12
- raw_connection.create_function('regexp', -1) do |func, pattern, expression, case_sensitive|
12
+ raw_connection.create_function("regexp", -1) do |func, pattern, expression, case_sensitive|
13
13
  options = 0
14
14
  options |= Regexp::IGNORECASE if case_sensitive.zero?
15
15
  pattern = Regexp.new(pattern.to_s, options)
@@ -7,5 +7,11 @@ module CafeCar
7
7
 
8
8
  self.error_status = :unprocessable_entity
9
9
  self.redirect_status = :see_other
10
+
11
+ def to_turbo_stream
12
+ # Put :html back in the accepted format list. respond_with removes it
13
+ controller.lookup_context.formats << :html
14
+ to_html
15
+ end
10
16
  end
11
17
  end
@@ -4,7 +4,7 @@ class CafeCar::Attributes
4
4
  def initialize(user, object, permitted_attributes)
5
5
  @user = user
6
6
  @object = object
7
- @permitted = [*permitted_attributes]
7
+ @permitted = [ *permitted_attributes ]
8
8
  process_attributes!
9
9
  end
10
10
 
@@ -16,7 +16,7 @@ module CafeCar
16
16
  TOPLEVEL_BINDING.eval <<~RUBY, __FILE__, __LINE__
17
17
  class #{mod.name}::#{name} < CafeCar[:ApplicationController]
18
18
  include CafeCar::Controller
19
- recline_in_the_cafe_car
19
+ cafe_car
20
20
  self
21
21
  end
22
22
  RUBY
@@ -1,68 +1,133 @@
1
1
  module CafeCar
2
2
  class Component
3
- include OptionHelpers
4
-
5
3
  concerning :Macros do
4
+ included do
5
+ class_attribute :option_defaults, default: {}
6
+ class_attribute :attribute_defaults, default: {}
7
+ end
8
+
9
+ def extract_options!(options)
10
+ @options = options.extract!(*option_defaults.keys).with_defaults!(option_defaults)
11
+ end
12
+
6
13
  class_methods do
14
+ def inherited(subclass)
15
+ super
16
+ subclass.option_defaults = option_defaults.deep_dup
17
+ subclass.attribute_defaults = attribute_defaults.deep_dup
18
+ end
19
+
7
20
  def component(*names, **, &)
8
21
  names.each do |name|
9
22
  define_class(name, CafeCar[:Component], **, &)
10
23
  end
11
24
  end
25
+
26
+ def flag(flag)
27
+ include Module.new do
28
+ define_method(flag) { |v| @options[flag] = v.nil? ? true : v }
29
+ end
30
+ end
31
+
32
+ def option(name, default: nil, accessor: true, reader: accessor, writer: accessor, presence: accessor, macro: accessor)
33
+ option_defaults[name] = default
34
+ define_singleton_method(name) { |v| option_defaults[name] = v } if macro
35
+ include Module.new {
36
+ define_method(name) { options[name] } if reader
37
+ define_method("#{name}=") { |v| options[name] = v } if writer
38
+ define_method("#{name}?") { options[name].present? } if presence
39
+ }
40
+ end
12
41
  end
13
42
  end
14
43
 
15
- attr_reader :flags, :options
44
+ def self.method_missing(name, v)
45
+ attribute_defaults[name] = v
46
+ end
16
47
 
17
- delegate :tag, :render, :capture, :safe_join, :ui_class, to: :@template
48
+ attr_reader :flags, :options, :attributes
18
49
 
19
- option :tag, default: :div
50
+ delegate :render, :capture, :safe_join, :ui_class, :context?, to: :@template
20
51
 
21
- def initialize(template, name, *args, **options, &block)
22
- @template = template
23
- @names = [*name].map(&:to_s).map(&:underscore).map(&:to_sym)
24
- @flags = args.extract! { _1.is_a? Symbol }
25
- @args = args.flatten.compact_blank
26
- @children = options.extract_if! { _1 =~ /^[A-Z]\w*$/ }
27
- @options = options
28
- @block = block
29
- options.transform!(:href) { @template.href_for _1 }
30
- assign_options!
52
+ option :tag, default: :div
53
+ option :class, accessor: false
54
+ option :data, default: {}
55
+ option :href
56
+ option :tip
57
+
58
+ def initialize(template, name, *args, **attributes, &block)
59
+ @template = template
60
+ @names = [ *name ].map(&:underscore)
61
+ @flags = args.extract! { _1.is_a? Symbol }
62
+ @args = args.flatten.compact_blank
63
+ @attributes = attributes.with_defaults!(attribute_defaults)
64
+ @children = attributes.extract_if! { _1 =~ /^[A-Z]\w*$/ }
65
+ @block = block
66
+ extract_options!(@attributes)
31
67
  end
32
68
 
33
69
  def name = @names.last
34
- def context = @context ||= Context.new(@template, prefix: @names)
35
- def partial? = @template.partial?(partial_name)
36
- def href? = options[:href].present?
37
- def tag_name = href? && !current_href? ? :a : @tag
70
+ def tag = href? ? :a : super
71
+ def href? = super && !context?(:a) && !current_href?
72
+
73
+ def href
74
+ @template.href_for(super) if href?
75
+ end
38
76
 
39
77
  def current_href? = options[:href]&.then { @template.current_href?(_1, check_parameters: true) }
40
78
  def ancestor_href? = options[:href]&.then { @template.ancestor_href?(_1) }
41
79
 
42
- def partial_name = 'ui/' + @names.join('_')
80
+ def data
81
+ super.merge({ tip: }.compact_blank)
82
+ end
43
83
 
44
- def class_names = @names.map(&:to_s).map(&:camelize)
45
- def class_name(...) = ui_class(class_names, *@flags, *(@tag.to_s if href?), (:current if current_href?), (:ancestor if ancestor_href?), ...)
84
+ def partial? = @template.partial?(partial_name)
85
+ def partial_name = "ui/" + @names.join("_")
86
+
87
+ def render_partial
88
+ render(partial_name, options:, flags:, name => self, **options) { content }
89
+ end
90
+
91
+ def selector = class_names.join(?_).then { ?. + _1 }
92
+
93
+ def class_names = @names.map(&:camelize)
94
+
95
+ def class_name
96
+ @template.ui_class(class_names, *flags, *options[:class], options[:tag].to_s => href?,
97
+ current: current_href?,
98
+ ancestor: ancestor_href? && !current_href?)
99
+ end
100
+
101
+ def attributes
102
+ @attributes.merge(class: class_name, href:, data:)
103
+ end
46
104
 
47
105
  def children
48
- @children.map {|name, content| send(name) { content } }
106
+ @children.map { |name, content| send(name) { content } }
107
+ end
108
+
109
+ def blank?
110
+ @block and body and content.blank? || !content.match?(/^.*?[^<\s]/) || content.gsub(/<!--.*?-->/, "").blank?
111
+ end
112
+
113
+ def body
114
+ @body ||= context { partial? ? render_partial : content }
49
115
  end
50
116
 
51
117
  def content
52
- @content ||= @template.safe_join([*children, *@args, *(capture(self, &@block) if @block)])
118
+ @content ||= safe_join [ *children, *@args, *(capture(self, &@block) if @block) ]
53
119
  end
54
120
 
55
- def wrapper(*args, **opts, &)
56
- @template.content_tag(tag_name, safe_join([*args]), class: class_name(*opts.delete(:class)), **opts) do
57
- capture(self, &)
58
- end
121
+ def context(&)
122
+ href? ? @template.context(:a, &) : capture(&)
59
123
  end
60
124
 
61
- def blank?
62
- content.blank? or !content.match?(/^.*?[^<\s]/) or content.gsub(/<!--.*?-->/, "").blank?
125
+ def wrapper(&)
126
+ @template.content_tag(tag, **attributes, &)
63
127
  end
64
128
 
65
- def +(o) = safe_join([self, o])
129
+ def ~@ = @template.concat(self)
130
+ def +(o) = safe_join([ self, o ])
66
131
  def <<(o) = @template.concat(o)
67
132
 
68
133
  def html_safe? = true
@@ -70,16 +135,14 @@ module CafeCar
70
135
  def to_s = to_html
71
136
 
72
137
  def to_html
73
- return "" if @block and blank?
74
-
75
- if partial?
76
- render(partial_name, options:, flags:, c: self, component: self, name => self, **options) { content }
77
- else
78
- wrapper(*@args, **@options) { content }
79
- end
138
+ return "" if blank?
139
+ wrapper { body }
80
140
  end
81
141
 
82
- def child(name, ...) = Component.new(@template, [*@names, name], ...)
142
+ def child(name, ...)
143
+ c = self.class.try { const_defined?(name) ? const_get(name) : Component }
144
+ c.new(@template, [ *@names, name ], ...)
145
+ end
83
146
 
84
147
  def method_missing(name, ...)
85
148
  if name =~ /^[A-Z]/
@@ -5,12 +5,13 @@ module CafeCar
5
5
  @prefix = prefix
6
6
  end
7
7
 
8
- def class(name, ...) = @template.ui_class([*@prefix, *name], ...)
9
- def wrapper(...) = Component.new(@template, [*@prefix], ...).wrapper(...)
8
+ def class(name, ...) = @template.ui_class([ *@prefix, *name ], ...)
9
+ # def wrapper(...) = Component.new(@template, [*@prefix], ...).wrapper(...)
10
10
  def <<(obj) = @template.concat(obj)
11
11
 
12
- def method_missing(method, ...)
13
- Component.new(@template, [*@prefix, method], ...)
12
+ def method_missing(name, ...)
13
+ c = CafeCar[:UI].const_try(name) || Component
14
+ c.new(@template, [ *@prefix, name ], ...)
14
15
  end
15
16
  end
16
17
  end
@@ -2,7 +2,7 @@ module CafeCar::Controller::Filtering
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
- helper_method :parsed_params
5
+ helper_method :parsed_params, :dot_params, :filtered?
6
6
  end
7
7
 
8
8
  private
@@ -11,6 +11,14 @@ module CafeCar::Controller::Filtering
11
11
  scope.query(parsed_params[""])
12
12
  end
13
13
 
14
+ def filtered?
15
+ parsed_params[""].present?
16
+ end
17
+
18
+ def dot_params
19
+ request.params.slice(*request.params.keys.grep(/^\./))
20
+ end
21
+
14
22
  def parsed_params
15
23
  @parsed_params ||=
16
24
  if request.get? || request.head?
@@ -5,21 +5,30 @@ module CafeCar
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  include Pundit::Authorization
8
- include Filtering
8
+ include Filtering, Authentication
9
9
 
10
10
  class_methods do
11
11
  def model(model)
12
12
  define_method(:model) { @model ||= model }
13
13
  end
14
14
 
15
- def recline_in_the_cafe_car(only: nil, except: nil)
15
+ def default_view(v = @default_view || "table")
16
+ @default_view = v.to_s
17
+ end
18
+
19
+ def cafe_car(only: nil, except: nil, model: nil)
16
20
  _only = ->(actions) do
17
21
  actions -= except if except
18
22
  actions &= only if only
19
23
  actions
20
24
  end
21
25
 
22
- rescue_from ::ActiveRecord::RecordInvalid, with: :render_invalid_record
26
+ self.model model if model
27
+
28
+ rescue_from ::ActiveRecord::RecordInvalid,
29
+ ::ActiveModel::ValidationError, with: :render_invalid_object
30
+
31
+ rescue_from ::Pundit::NotAuthorizedError, with: :render_unauthorized
23
32
 
24
33
  append_cafe_car_views
25
34
 
@@ -35,8 +44,24 @@ module CafeCar
35
44
  end
36
45
 
37
46
  def append_cafe_car_views
38
- append_view_path CafeCar::Engine.root.join('app/views/cafe_car')
39
- append_view_path 'app/views/cafe_car'
47
+ append_view_path CafeCar::Engine.root.join("app/views/cafe_car")
48
+ append_view_path "app/views/cafe_car"
49
+ end
50
+
51
+ def define_callbacks_with_helpers(*names, **)
52
+ define_callbacks(*names, **)
53
+
54
+ names.each do |name|
55
+ %i[before around after].each do |kind|
56
+ define_singleton_method "#{kind}_#{name}" do |*args, **opts|
57
+ set_callback(name, kind, *args, prepend: true, **opts)
58
+ end
59
+
60
+ define_singleton_method "skip_#{kind}_#{name}" do |*args|
61
+ skip_callback(name, kind, *args)
62
+ end
63
+ end
64
+ end
40
65
  end
41
66
  end
42
67
 
@@ -44,11 +69,11 @@ module CafeCar
44
69
  self.responder = CafeCar[:ApplicationResponder]
45
70
  default_form_builder CafeCar[:FormBuilder]
46
71
 
47
- define_callbacks :render, :update, :create, :destroy
72
+ define_callbacks_with_helpers :render, :update, :create, :destroy
48
73
  add_flash_types :success, :warning, :error
49
74
 
50
75
  helper_method :model, :model_name, :object, :objects
51
- helper_method :action, :scope, :view
76
+ helper_method :action, :scope, :view, :default_view
52
77
 
53
78
  helper Helpers
54
79
  delegate :present, :href_for, :namespace, to: :helpers
@@ -92,8 +117,6 @@ module CafeCar
92
117
  flash[:success] = present(object).i18n("#{action_name}_html", scope: :flashes)
93
118
  end
94
119
 
95
- def current_user = CafeCar[:Current].user
96
-
97
120
  def sorted(scope)
98
121
  scope.sorted(*params[:sort].presence)
99
122
  end
@@ -139,12 +162,24 @@ module CafeCar
139
162
  def model_name = model.model_name
140
163
 
141
164
  def model
142
- @model ||= self.class.name.gsub(/.*::|Controller$/, '')
165
+ @model ||= self.class.name.gsub(/.*::|Controller$/, "")
143
166
  .classify
144
167
  .then { self.class.module_parent.const_get _1 }
145
168
  end
146
169
 
147
- def render_invalid_record = render(object.persisted? ? 'edit' : 'new', status: :unprocessable_content)
170
+ def render_invalid_object
171
+ render(object.persisted? ? "edit" : "new", status: :unprocessable_content)
172
+ end
173
+
174
+ def render_unauthorized
175
+ if !sessions_available?
176
+ head :forbidden
177
+ elsif authenticated?
178
+ redirect_back_or_to root_path
179
+ else
180
+ request_authentication
181
+ end
182
+ end
148
183
 
149
184
  # def default_render(...) = run_callbacks(:render) { super }
150
185
 
@@ -156,12 +191,16 @@ module CafeCar
156
191
  @action ||= ActiveSupport::StringInquirer.new(action_name)
157
192
  end
158
193
 
194
+ def default_view = self.class.default_view
159
195
  def view
160
- params.fetch(:view) { "table" }
196
+ params.fetch(:view) { default_view }
161
197
  end
162
198
 
163
199
  def _render_with_renderer_json(obj, options)
164
- options[:only] ||= [:id] | policy(obj).displayable_attributes
200
+ # permitted_attributes is record-oriented, so ask a record for the column
201
+ # list even when serializing a collection.
202
+ record = obj.is_a?(CafeCar::Model) ? obj : obj.klass.new
203
+ options[:only] ||= [ :id ] | policy(record).displayable_attributes
165
204
 
166
205
  if obj.is_a?(CafeCar::Model)
167
206
  options[:include] ||= policy(obj).displayable_associations
@@ -8,4 +8,17 @@ class Array
8
8
  end
9
9
  end
10
10
  end
11
+
12
+ def extract!(pattern = nil, &block)
13
+ block = -> { pattern === _1 } if pattern
14
+ return to_enum(:extract!) { size } unless block
15
+
16
+ extracted_elements = []
17
+
18
+ reject! do |element|
19
+ extracted_elements << element if block.(element)
20
+ end
21
+
22
+ extracted_elements
23
+ end
11
24
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hash
4
+ # Removes and returns a hash containing the key/value pairs for which the
5
+ # block returns a true value given the key.
6
+ #
7
+ # hash = {:a => 1, "b" => 2, :c => 3}
8
+ # hash.extract_if! { _1.is_a? Symbol } #=> {a: 1, c: 3}
9
+ # hash #=> {"b" => 2 }
10
+ def extract_if!
11
+ each_with_object(self.class.new) do |(key, value), result|
12
+ result[key] = delete(key) if yield(key, value)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Module
4
+ # Shorthand for `const_set(name.camelize, Class.new(parent) { ... })`.
5
+ # Useful when defining classes in macros.
6
+ def define_class(name, *, **macros, &)
7
+ name = name.to_s.camelize
8
+ raise NameError, "class exists: #{name}" if const_defined?(name, false)
9
+ klass = Class.new(*) do
10
+ macros.each { |key, value| send(key, *value) }
11
+ class_eval(&) if block_given?
12
+ end
13
+ const_set name, klass
14
+ end
15
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cnc/core_ext"
4
-
5
3
  Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).sort.each do |path|
6
4
  require path
7
5
  end
@@ -1,6 +1,9 @@
1
1
  module CafeCar
2
2
  class Current < ActiveSupport::CurrentAttributes
3
+ delegate :user, to: :session, allow_nil: true
4
+
3
5
  attribute :request_id, :user_agent, :ip_address
4
- attribute :user
6
+ attribute :session
7
+ # attribute :user
5
8
  end
6
9
  end
@@ -6,6 +6,8 @@ require "propshaft"
6
6
  require "pundit"
7
7
  require "importmap-rails"
8
8
  require "turbo-rails"
9
+ require "potter"
10
+ require "potter/type"
9
11
 
10
12
  module CafeCar
11
13
  class Engine < ::Rails::Engine
@@ -72,13 +74,18 @@ module CafeCar
72
74
  end
73
75
  end
74
76
 
77
+ initializer "cafe_car.responders" do
78
+ require "responders"
79
+ config.responders.flash_keys = [ :success, :error ]
80
+ end
81
+
75
82
  initializer "cafe_car.field_with_errors" do
76
83
  ActionView::Base.field_error_proc = proc { _1.html_safe }
77
84
  end
78
85
 
79
86
  initializer "cafe_car.console" do |app|
80
87
  app.console do
81
- TOPLEVEL_BINDING.eval('self').instance_exec do
88
+ TOPLEVEL_BINDING.eval("self").instance_exec do
82
89
  def logger = Rails.logger
83
90
 
84
91
  if defined?(FactoryBot)
@@ -86,7 +93,7 @@ module CafeCar
86
93
  logger.info "FactoryBot methods enabled."
87
94
  end
88
95
 
89
- logger.info 'SQL logs enabled.'
96
+ logger.info "SQL logs enabled."
90
97
 
91
98
  ApplicationController.allow_forgery_protection = false
92
99
  logger.info "CSRF disabled to enable app.post calls."
@@ -29,7 +29,7 @@ module CafeCar
29
29
 
30
30
  private
31
31
 
32
- def add_class(*args, opts) = {class: @template.ui_class([:field, *args], *opts.delete(:class)), **opts}
32
+ def add_class(*args, opts) = { class: @template.ui_class([ :field, *args ], *opts.delete(:class)), **opts }
33
33
 
34
34
  def send_to_form_with_text(method, text = @options.delete(method), **, &)
35
35
  return if text == false
@@ -13,8 +13,9 @@ module CafeCar
13
13
 
14
14
  def info(method) = model.info.field(method)
15
15
 
16
- def id? = method =~ /_ids?$/
17
- def constant? = method.in? %i[id created_at updated_at]
16
+ def id? = method =~ /_ids?$/
17
+ def constant? = method.in? %i[id created_at updated_at]
18
+ def timestamp? = type.in? %i[date datetime time]
18
19
 
19
20
  def association?
20
21
  return if @method.nil?
@@ -30,11 +31,11 @@ module CafeCar
30
31
  def collection = reflection.klass.all
31
32
  def reflection
32
33
  return if @method.nil?
33
- model.reflect_on_association(@method) || reflections_by_attribute[@method]
34
+ model.try(:reflect_on_association, @method) || reflections_by_attribute[@method]
34
35
  end
35
36
 
36
37
  def abrogated_keys
37
- [*reflection&.foreign_type&.to_sym]
38
+ [ *reflection&.foreign_type&.to_sym ]
38
39
  end
39
40
 
40
41
  def displayable = reflection&.name&.then { info(_1) } || self
@@ -61,7 +62,7 @@ module CafeCar
61
62
  model.nested_attributes_options.key?(key) && :nested
62
63
  end
63
64
 
64
- def polymorphic_methods = [reflection.foreign_type, reflection.foreign_key]
65
+ def polymorphic_methods = [ reflection.foreign_type, reflection.foreign_key ]
65
66
 
66
67
  def placeholder = i18n(:placeholder)
67
68
  def autocomplete = i18n(:autocomplete)
@@ -78,7 +79,7 @@ module CafeCar
78
79
  def i18n_key = model_name.i18n_key
79
80
  def i18n(key, **opts)
80
81
  return if @method.nil?
81
- I18n.t(@method, scope: [:helpers, key, i18n_key], raise: true, **opts)
82
+ I18n.t(@method, scope: [ :helpers, key, i18n_key ], raise: true, **opts)
82
83
  rescue I18n::MissingTranslationData
83
84
  end
84
85
 
@@ -95,7 +96,7 @@ module CafeCar
95
96
  # "minmax(10em, fit-content)"
96
97
  # "minmax(10em, 1fr)"
97
98
  "minmax(10em, auto)"
98
- # "min-content"
99
+ # "minmax(min-content, auto)"
99
100
  else "min-content"
100
101
  end
101
102
  end
@@ -127,13 +128,14 @@ module CafeCar
127
128
 
128
129
  @@reflections_by_attribute = {}
129
130
  def reflections_by_attribute
131
+ return {} unless model.respond_to? :reflections
130
132
  @@reflections_by_attribute[model] ||=
131
133
  model.reflections.values.index_by do |r|
132
- case [r.macro, r.name]
133
- in [:belongs_to, *] then r.foreign_key
134
- in [:has_many, *] then "#{r.name.to_s.singularize}_ids"
135
- in [:has_one, /^rich_text_(\w+)$/] then $1
136
- in [:has_one, *] then r.name
134
+ case [ r.macro, r.name ]
135
+ in [ :belongs_to, * ] then r.foreign_key
136
+ in [ :has_many, * ] then "#{r.name.to_s.singularize}_ids"
137
+ in [ :has_one, /^rich_text_(\w+)$/ ] then $1
138
+ in [ :has_one, * ] then r.name
137
139
  else raise NoMethodError.new("Not yet implemented :#{r.macro}")
138
140
  end
139
141
  end.with_indifferent_access
@@ -5,10 +5,17 @@ module CafeCar
5
5
  derive :editable, -> { Fields.new reject(&:constant?) }
6
6
  derive :listable, -> { Fields.new editable.reject(&:digest?) }
7
7
  derive :attachments, -> { Fields.new select(&:attachment?) }
8
+ derive :timestamps, -> { Fields.new select(&:timestamp?) }
8
9
 
9
10
  derive :by_name, -> { index_by(&:name).with_indifferent_access }
10
11
  derive :names, -> { map(&:name) }
11
12
 
13
+ def reverse = Fields.new(super)
14
+
15
+ def sort_with(obj)
16
+ Fields.new(sort_by { obj.try(_1.name) })
17
+ end
18
+
12
19
  def has?(name) = by_name.key?(name)
13
20
  end
14
21
  end
@@ -1,6 +1,6 @@
1
1
  class CafeCar::Filter::FieldInfo < CafeCar[:FieldInfo]
2
2
  def i18n(key, **opts)
3
- I18n.t(@method, scope: [:helpers, :filter, key, i18n_key], raise: true, **opts)
3
+ I18n.t(@method, scope: [ :helpers, :filter, key, i18n_key ], raise: true, **opts)
4
4
  rescue I18n::MissingTranslationData
5
5
  end
6
6
 
@@ -10,12 +10,12 @@ module CafeCar::Filter
10
10
  dotted_name method
11
11
  end
12
12
 
13
- def clean(method) = method.to_s.sub(/^\W+|\W+$/, '')
13
+ def clean(method) = method.to_s.sub(/^\W+|\W+$/, "")
14
14
  def info(method) = super(clean(method))
15
15
 
16
16
  def field_name(*methods, multiple: false, index: @options[:index])
17
17
  # TODO: handle multiple/index
18
- ["", *methods].join(".")
18
+ [ "", *methods ].join(".")
19
19
  end
20
20
  end
21
21
  end
@@ -8,7 +8,7 @@ module CafeCar
8
8
  end
9
9
 
10
10
  def model = @objects.klass
11
- def to_key = [model_name.param_key, :filters]
11
+ def to_key = [ model_name.param_key, :filters ]
12
12
  def to_model = self
13
13
  def persisted? = false
14
14
  def errors = Hash.new([])