cafe_car 0.1.0 → 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 (240) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +682 -8
  3. data/Rakefile +21 -0
  4. data/app/assets/fonts/Lexend.css +7 -0
  5. data/app/assets/fonts/Lexend.ttf +0 -0
  6. data/app/assets/images/noise.svg +16 -0
  7. data/app/assets/stylesheets/actiontext.css +31 -0
  8. data/app/assets/stylesheets/application.css +1 -0
  9. data/app/assets/stylesheets/cafe_car/code/base16-dark.css +89 -0
  10. data/app/assets/stylesheets/cafe_car/code/base16-light.css +90 -0
  11. data/app/assets/stylesheets/cafe_car/pagination.css +5 -0
  12. data/app/assets/stylesheets/cafe_car/themes/cool.css +32 -0
  13. data/app/assets/stylesheets/cafe_car/themes/cool2.css +31 -0
  14. data/app/assets/stylesheets/cafe_car/themes/defaults.css +60 -0
  15. data/app/assets/stylesheets/cafe_car/themes/warm-dark.css +29 -0
  16. data/app/assets/stylesheets/cafe_car/themes/warm.css +24 -0
  17. data/app/assets/stylesheets/cafe_car/tooltips.css +20 -0
  18. data/app/assets/stylesheets/cafe_car/trix.css +56 -0
  19. data/app/assets/stylesheets/cafe_car/utility.css +63 -0
  20. data/app/assets/stylesheets/cafe_car.css +96 -0
  21. data/app/assets/stylesheets/iconoir.css +22 -0
  22. data/app/assets/stylesheets/ui/Alert.css +25 -0
  23. data/app/assets/stylesheets/ui/Article.css +11 -0
  24. data/app/assets/stylesheets/ui/Button.css +42 -0
  25. data/app/assets/stylesheets/ui/Card.css +74 -0
  26. data/app/assets/stylesheets/ui/Chat.css +33 -0
  27. data/app/assets/stylesheets/ui/Close.css +11 -0
  28. data/app/assets/stylesheets/ui/Code.css +4 -0
  29. data/app/assets/stylesheets/ui/Controls.css +16 -0
  30. data/app/assets/stylesheets/ui/Error.css +3 -0
  31. data/app/assets/stylesheets/ui/Example.css +45 -0
  32. data/app/assets/stylesheets/ui/Field.css +31 -0
  33. data/app/assets/stylesheets/ui/Grid.css +6 -0
  34. data/app/assets/stylesheets/ui/Group.css +16 -0
  35. data/app/assets/stylesheets/ui/Icon.css +27 -0
  36. data/app/assets/stylesheets/ui/Image.css +14 -0
  37. data/app/assets/stylesheets/ui/InfoCircle.css +11 -0
  38. data/app/assets/stylesheets/ui/Input.css +36 -0
  39. data/app/assets/stylesheets/ui/Layout.css +100 -0
  40. data/app/assets/stylesheets/ui/Menu.css +38 -0
  41. data/app/assets/stylesheets/ui/Modal.css +26 -0
  42. data/app/assets/stylesheets/ui/Navigation.css +37 -0
  43. data/app/assets/stylesheets/ui/Page.css +105 -0
  44. data/app/assets/stylesheets/ui/Row.css +9 -0
  45. data/app/assets/stylesheets/ui/Table.css +101 -0
  46. data/app/assets/stylesheets/ui/components.css +24 -0
  47. data/app/controllers/cafe_car/application_controller.rb +9 -0
  48. data/app/controllers/cafe_car/examples_controller.rb +22 -0
  49. data/app/controllers/cafe_car/sessions_controller.rb +30 -0
  50. data/app/controllers/concerns/cafe_car/authentication.rb +61 -0
  51. data/app/javascript/application.js +5 -0
  52. data/app/javascript/cafe_car.js +174 -0
  53. data/app/models/cafe_car/session.rb +18 -0
  54. data/app/policies/cafe_car/application_policy.rb +42 -0
  55. data/app/policies/cafe_car/session_policy.rb +19 -0
  56. data/app/presenters/cafe_car/action_text/rich_text_presenter.rb +7 -0
  57. data/app/presenters/cafe_car/active_record/base_presenter.rb +6 -0
  58. data/app/presenters/cafe_car/active_record/relation_presenter.rb +17 -0
  59. data/app/presenters/cafe_car/active_storage/attached/one_presenter.rb +9 -0
  60. data/app/presenters/cafe_car/active_storage/attachment_presenter.rb +18 -0
  61. data/app/presenters/cafe_car/basic_object_presenter.rb +5 -0
  62. data/app/presenters/cafe_car/code_presenter.rb +18 -0
  63. data/app/presenters/cafe_car/currency_presenter.rb +5 -0
  64. data/app/presenters/cafe_car/date_and_time/compatibility_presenter.rb +6 -0
  65. data/app/presenters/cafe_car/date_presenter.rb +5 -0
  66. data/app/presenters/cafe_car/date_time_presenter.rb +11 -0
  67. data/app/presenters/cafe_car/enumerable_presenter.rb +13 -0
  68. data/app/presenters/cafe_car/false_class_presenter.rb +5 -0
  69. data/app/presenters/cafe_car/hash_presenter.rb +6 -0
  70. data/app/presenters/cafe_car/nil_class_presenter.rb +13 -0
  71. data/app/presenters/cafe_car/presenter.rb +157 -0
  72. data/app/presenters/cafe_car/range_presenter.rb +16 -0
  73. data/app/presenters/cafe_car/record_presenter.rb +5 -0
  74. data/app/presenters/cafe_car/string_presenter.rb +20 -0
  75. data/app/presenters/cafe_car/symbol_presenter.rb +5 -0
  76. data/app/presenters/cafe_car/true_class_presenter.rb +5 -0
  77. data/app/ui/cafe_car/ui/button.rb +9 -0
  78. data/app/ui/cafe_car/ui/card.rb +18 -0
  79. data/app/ui/cafe_car/ui/field.rb +11 -0
  80. data/app/ui/cafe_car/ui/grid.rb +30 -0
  81. data/app/ui/cafe_car/ui/layout.rb +7 -0
  82. data/app/ui/cafe_car/ui/page.rb +14 -0
  83. data/app/views/application/_actions.html.haml +1 -0
  84. data/app/views/application/_alerts.html.haml +2 -0
  85. data/app/views/application/_body.html.haml +7 -0
  86. data/app/views/application/_controls.html.haml +12 -0
  87. data/app/views/application/_debug.html.haml +18 -0
  88. data/app/views/application/_empty.html.haml +1 -0
  89. data/app/views/application/_errors.html.haml +4 -0
  90. data/app/views/application/_field.html.haml +5 -0
  91. data/app/views/application/_fields.html.haml +1 -0
  92. data/app/views/application/_filters.html.haml +8 -0
  93. data/app/views/application/_form.html.haml +6 -0
  94. data/app/views/application/_grid.html.haml +3 -0
  95. data/app/views/application/_grid_item.html.haml +1 -0
  96. data/app/views/application/_head.html.haml +17 -0
  97. data/app/views/application/_index.html.haml +8 -0
  98. data/app/views/application/_index_actions.html.haml +7 -0
  99. data/app/views/application/_navigation.html.haml +9 -0
  100. data/app/views/application/_navigation_links.html.haml +5 -0
  101. data/app/views/application/_notes.html.haml +10 -0
  102. data/app/views/application/_popup.html.haml +7 -0
  103. data/app/views/application/_show.html.haml +9 -0
  104. data/app/views/application/_submit.html.haml +1 -0
  105. data/app/views/application/_table.html.haml +6 -0
  106. data/app/views/cafe_car/application/create.turbo_stream.haml +2 -0
  107. data/app/views/cafe_car/application/destroy.turbo_stream.haml +1 -0
  108. data/app/views/cafe_car/application/edit.html.haml +18 -0
  109. data/app/views/cafe_car/application/edit.turbo_stream.haml +7 -0
  110. data/app/views/cafe_car/application/index.html.haml +15 -0
  111. data/app/views/cafe_car/application/new.html.haml +8 -0
  112. data/app/views/cafe_car/application/new.turbo_stream.haml +8 -0
  113. data/app/views/cafe_car/application/show.html.haml +36 -0
  114. data/app/views/cafe_car/application/update.turbo_stream.haml +2 -0
  115. data/app/views/cafe_car/examples/_example.html.haml +12 -0
  116. data/app/views/cafe_car/examples/_index.html.haml +16 -0
  117. data/app/views/cafe_car/examples/_navigation_links.html.haml +2 -0
  118. data/app/views/cafe_car/examples/ui/_alert.html.haml +4 -0
  119. data/app/views/cafe_car/examples/ui/_button.html.haml +3 -0
  120. data/app/views/cafe_car/examples/ui/_card.html.haml +6 -0
  121. data/app/views/cafe_car/examples/ui/_chat.html.haml +3 -0
  122. data/app/views/cafe_car/examples/ui/_controls.html.haml +3 -0
  123. data/app/views/cafe_car/examples/ui/_error.html.haml +1 -0
  124. data/app/views/cafe_car/examples/ui/_field.html.haml +9 -0
  125. data/app/views/cafe_car/examples/ui/_grid.html.haml +11 -0
  126. data/app/views/cafe_car/examples/ui/_group.html.haml +21 -0
  127. data/app/views/cafe_car/examples/ui/_info_circle.html.haml +1 -0
  128. data/app/views/cafe_car/examples/ui/_menu.html.haml +5 -0
  129. data/app/views/cafe_car/examples/ui/_modal.html.haml +4 -0
  130. data/app/views/cafe_car/examples/ui/_navigation.html.haml +4 -0
  131. data/app/views/cafe_car/examples/ui/_page.html.haml +4 -0
  132. data/app/views/cafe_car/examples/ui/_table.html.haml +13 -0
  133. data/app/views/cafe_car/layouts/mailer.html.haml +8 -0
  134. data/app/views/cafe_car/layouts/mailer.text.erb +1 -0
  135. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  136. data/app/views/layouts/application.html.haml +4 -0
  137. data/app/views/layouts/mailer.html.erb +13 -0
  138. data/app/views/layouts/mailer.text.erb +1 -0
  139. data/app/views/notes/_fields.html.haml +1 -0
  140. data/app/views/passwords_mailer/reset.html.haml +5 -0
  141. data/app/views/passwords_mailer/reset.text.erb +4 -0
  142. data/app/views/ui/_card.html.haml +8 -0
  143. data/app/views/ui/_field.html.haml +1 -0
  144. data/app/views/ui/_modal_close.html.haml +1 -0
  145. data/app/views/ui/_page.html.haml +7 -0
  146. data/config/brakeman.ignore +77 -0
  147. data/config/importmap.rb +12 -0
  148. data/config/locales/en.yml +63 -0
  149. data/config/routes.rb +9 -0
  150. data/db/migrate/20251005220017_create_slugs.rb +13 -0
  151. data/lib/cafe_car/active_record.rb +21 -0
  152. data/lib/cafe_car/application_responder.rb +17 -0
  153. data/lib/cafe_car/attributes.rb +23 -0
  154. data/lib/cafe_car/auto_resolver.rb +49 -0
  155. data/lib/cafe_car/caching.rb +20 -0
  156. data/lib/cafe_car/component.rb +155 -0
  157. data/lib/cafe_car/context.rb +17 -0
  158. data/lib/cafe_car/controller/filtering.rb +30 -0
  159. data/lib/cafe_car/controller.rb +218 -0
  160. data/lib/cafe_car/core_ext/array.rb +24 -0
  161. data/lib/cafe_car/core_ext/hash.rb +15 -0
  162. data/lib/cafe_car/core_ext/module.rb +15 -0
  163. data/lib/cafe_car/core_ext.rb +5 -0
  164. data/lib/cafe_car/current.rb +9 -0
  165. data/lib/cafe_car/engine.rb +107 -0
  166. data/lib/cafe_car/field_builder.rb +44 -0
  167. data/lib/cafe_car/field_info.rb +144 -0
  168. data/lib/cafe_car/fields.rb +21 -0
  169. data/lib/cafe_car/filter/field_builder.rb +4 -0
  170. data/lib/cafe_car/filter/field_info.rb +22 -0
  171. data/lib/cafe_car/filter/form_builder.rb +21 -0
  172. data/lib/cafe_car/filter.rb +5 -0
  173. data/lib/cafe_car/filter_builder.rb +20 -0
  174. data/lib/cafe_car/form_builder.rb +105 -0
  175. data/lib/cafe_car/generators.rb +30 -0
  176. data/lib/cafe_car/helpers.rb +178 -0
  177. data/lib/cafe_car/href_builder.rb +97 -0
  178. data/lib/cafe_car/informable.rb +9 -0
  179. data/lib/cafe_car/input_builder.rb +25 -0
  180. data/lib/cafe_car/inputs/association_builder.rb +6 -0
  181. data/lib/cafe_car/inputs/base_input.rb +19 -0
  182. data/lib/cafe_car/inputs/belongs_to_builder.rb +6 -0
  183. data/lib/cafe_car/inputs/password_input.rb +7 -0
  184. data/lib/cafe_car/inputs/string_input.rb +7 -0
  185. data/lib/cafe_car/link_builder.rb +65 -0
  186. data/lib/cafe_car/model.rb +23 -0
  187. data/lib/cafe_car/model_info.rb +24 -0
  188. data/lib/cafe_car/name_patch.rb +17 -0
  189. data/lib/cafe_car/navigation.rb +76 -0
  190. data/lib/cafe_car/option_helpers.rb +53 -0
  191. data/lib/cafe_car/param_parser.rb +45 -0
  192. data/lib/cafe_car/pluralization.rb +15 -0
  193. data/lib/cafe_car/policy.rb +77 -0
  194. data/lib/cafe_car/proc_helpers.rb +13 -0
  195. data/lib/cafe_car/query_builder.rb +186 -0
  196. data/lib/cafe_car/queryable.rb +29 -0
  197. data/lib/cafe_car/resolver.rb +27 -0
  198. data/lib/cafe_car/routing.rb +17 -0
  199. data/lib/cafe_car/table/body_builder.rb +12 -0
  200. data/lib/cafe_car/table/builder.rb +52 -0
  201. data/lib/cafe_car/table/foot_builder.rb +14 -0
  202. data/lib/cafe_car/table/head_builder.rb +26 -0
  203. data/lib/cafe_car/table/label_builder.rb +48 -0
  204. data/lib/cafe_car/table/objects_builder.rb +8 -0
  205. data/lib/cafe_car/table/row_builder.rb +39 -0
  206. data/lib/cafe_car/table_builder.rb +13 -0
  207. data/lib/cafe_car/turbo_tag_builder.rb +7 -0
  208. data/lib/cafe_car/ui.rb +11 -0
  209. data/lib/cafe_car/version.rb +1 -1
  210. data/lib/cafe_car/visitors.rb +21 -0
  211. data/lib/cafe_car.rb +25 -168
  212. data/lib/generators/cafe_car/controller/USAGE +11 -0
  213. data/lib/generators/cafe_car/controller/controller_generator.rb +26 -0
  214. data/lib/generators/cafe_car/controller/templates/controller.rb.tt +5 -0
  215. data/lib/generators/cafe_car/install/USAGE +8 -0
  216. data/lib/generators/cafe_car/install/install_generator.rb +46 -0
  217. data/lib/generators/cafe_car/install/templates/application_policy.rb.tt +7 -0
  218. data/lib/generators/cafe_car/notes/USAGE +12 -0
  219. data/lib/generators/cafe_car/notes/notes_generator.rb +13 -0
  220. data/lib/generators/cafe_car/notes/templates/create_notes.rb.tt +12 -0
  221. data/lib/generators/cafe_car/notes/templates/notable.rb.tt +7 -0
  222. data/lib/generators/cafe_car/notes/templates/note.rb.tt +6 -0
  223. data/lib/generators/cafe_car/policy/USAGE +8 -0
  224. data/lib/generators/cafe_car/policy/policy_generator.rb +39 -0
  225. data/lib/generators/cafe_car/policy/templates/policy.rb.tt +20 -0
  226. data/lib/generators/cafe_car/resource/USAGE +13 -0
  227. data/lib/generators/cafe_car/resource/resource_generator.rb +32 -0
  228. data/lib/generators/cafe_car/sessions/USAGE +17 -0
  229. data/lib/generators/cafe_car/sessions/sessions_generator.rb +29 -0
  230. data/lib/generators/cafe_car/sessions/templates/create_sessions.rb.tt +12 -0
  231. data/lib/tasks/holdco_tasks.rake +532 -0
  232. data/lib/tasks/templates/tasks_header.md +37 -0
  233. metadata +444 -21
  234. data/app/views/cafe_car/application/_fields.html.erb +0 -7
  235. data/app/views/cafe_car/application/_filters.html.erb +0 -0
  236. data/app/views/cafe_car/application/_form.html.erb +0 -22
  237. data/lib/cafe_car/railtie.rb +0 -4
  238. /data/app/views/{cafe_car/application/_actions.html.erb → application/_aside.html.haml} +0 -0
  239. /data/app/views/{cafe_car/application/_aside.html.erb → application/_footer.html.haml} +0 -0
  240. /data/app/views/cafe_car/{application/_extra_fields.html.erb → examples/_index_actions.html.haml} +0 -0
@@ -0,0 +1,27 @@
1
+ module CafeCar
2
+ module Resolver
3
+ extend ActiveSupport::Concern
4
+
5
+ def const(name) = self.class.const(name)
6
+ def const!(name) = self.class.const!(name)
7
+
8
+ class_methods do
9
+ def const_scopes = [ self, *module_parents ]
10
+
11
+ def [](const_name) = const(const_name)
12
+
13
+ def const_try(name)
14
+ const_get(name) if const_defined?(name)
15
+ end
16
+
17
+ def const(name)
18
+ const_scopes.detect { _1.const_defined?(name) }
19
+ &.then { _1.const_get(name) }
20
+ end
21
+
22
+ def const!(name)
23
+ const(name) or raise NameError, "uninitialized constant #{name}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ module CafeCar
2
+ module Routing
3
+ extend ActiveSupport::Concern
4
+
5
+ def resources(*, concerns: [], **, &)
6
+ @concerns[:batchable] || begin
7
+ concern :batchable do
8
+ collection do
9
+ patch :update_all
10
+ end
11
+ end
12
+ end
13
+
14
+ super(*, **, concerns: [ :batchable, *concerns ], &)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module CafeCar::Table
2
+ class BodyBuilder < ObjectsBuilder
3
+ def to_html
4
+ ui.Body id: @objects.model_name.plural do |body|
5
+ body << @template.turbo_stream_from(model_name.plural)
6
+ @objects.each do |object|
7
+ body << RowBuilder.new(@template, object:, **@options, &@block)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,52 @@
1
+ module CafeCar::Table
2
+ class Builder
3
+ include CafeCar::ProcHelpers
4
+ include CafeCar::OptionHelpers
5
+
6
+ attr_reader :ui
7
+ delegate :capture, :link_to, :href_for, :params, :present, :request, :safe_join, to: :@template
8
+
9
+ def initialize(template, **options, &block)
10
+ @template = template
11
+ @options = options
12
+ @block = block
13
+ @ui = @template.ui.Table
14
+ @shown_attributes = {}
15
+ assign_options!
16
+ end
17
+
18
+ def model_name = model.model_name
19
+
20
+ def cell(method, ...) = shown!(method).then { nil }
21
+ def shown!(method) = @shown_attributes[method] = true
22
+ def shown = @shown_attributes
23
+
24
+ def has?(method) = model.info.fields.has?(method)
25
+
26
+ def title(method = policy.title_attribute, *, **, &)
27
+ cell(method, *, href: true, blank: "(none)", **, &)
28
+ end
29
+
30
+ def logo(method = policy.logo_attribute, *, **, &)
31
+ cell(method, *, label: nil, blank: "", **, &) if method
32
+ end
33
+
34
+ def timestamps(...) = cell(timestamp_attribute, ...)
35
+
36
+ def timestamp_attribute = %w[updated_at created_at].find { has? _1 }
37
+
38
+ def remaining_attributes = policy.listable_attributes - @shown_attributes.keys
39
+
40
+ def remaining(except: [])
41
+ capture do
42
+ (remaining_attributes - [ *except ]).each do |attr|
43
+ ui << cell(policy.info(attr).displayable.method)
44
+ end
45
+ end
46
+ end
47
+
48
+ def html_safe? = true
49
+ def to_s = @to_s ||= to_html.to_s
50
+ def ~@ = @template.concat(to_s)
51
+ end
52
+ end
@@ -0,0 +1,14 @@
1
+ module CafeCar::Table
2
+ class FootBuilder < ObjectsBuilder
3
+ def initialize(...)
4
+ super
5
+ @count = 0
6
+ end
7
+
8
+ def cell! = (@count += 1; nil)
9
+ def cell(...) = (super; cell!)
10
+ def controls(...) = cell!
11
+
12
+ def to_html = ""
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ module CafeCar::Table
2
+ class HeadBuilder < ObjectsBuilder
3
+ attr_reader :fields
4
+
5
+ def initialize(...)
6
+ @fields = []
7
+ super
8
+ end
9
+
10
+ def cell(method, *flags, label: label(method), **, &)
11
+ super
12
+ @fields << model.info.field(method)
13
+ ui.Cell(*flags) { label }
14
+ end
15
+
16
+ def label(method)
17
+ l = LabelBuilder.new(@template, objects: @objects, method:)
18
+ @objects.includes!(method) if l.association?
19
+ l
20
+ end
21
+
22
+ def controls(*, **) = cell(:controls, :controls, *, label: nil, **)
23
+
24
+ def to_html = ui.Head(:sticky) { capture(self, &@block) }
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ module CafeCar::Table
2
+ class LabelBuilder < ObjectsBuilder
3
+ def initialize(...)
4
+ super
5
+ @method = @options.delete(:method) { raise }
6
+ @key = @method.to_s
7
+ @key += ?. + title if belongs_to?
8
+ end
9
+
10
+ def field = policy.info(@method)
11
+ def title = reflection&.klass&.then { policy(_1).title_attribute&.to_s } rescue ""
12
+ def column? = @objects.columns_hash.key? @key
13
+ def sortable? = column? || belongs_to?
14
+ def reflection = field.reflection
15
+ def association? = field.association?
16
+ def belongs_to? = reflection&.belongs_to?
17
+ def order_value = @objects.order_values.first&.then { _1.expr.name }
18
+ def existing = params.fetch(:sort) { order_value }
19
+
20
+ def sort
21
+ @sort ||=
22
+ case existing
23
+ when @key then "-#{existing}"
24
+ else @key
25
+ end
26
+ end
27
+
28
+ def href
29
+ @template.url_for(**request.params.merge(sort:)) if sortable?
30
+ end
31
+
32
+ def label_sort
33
+ @template.tag.span(symbol) if sortable?
34
+ end
35
+
36
+ def symbol
37
+ case existing
38
+ when @key then "↑" # ▴↑▲
39
+ when "-#{@key}" then "↓" # ▾↓▼
40
+ else "•"
41
+ end
42
+ end
43
+
44
+ def label = @method&.then { present(@objects).human(_1) }
45
+ def content = @template.safe_join([ label, label_sort ], "&nbsp;".html_safe)
46
+ def to_html = @template.link_to_unless(!sortable?, content, href)
47
+ end
48
+ end
@@ -0,0 +1,8 @@
1
+ module CafeCar::Table
2
+ class ObjectsBuilder < Builder
3
+ option :objects, default: -> { raise }
4
+
5
+ def model = @objects
6
+ def policy(o = @objects) = @template.policy(o)
7
+ end
8
+ end
@@ -0,0 +1,39 @@
1
+ module CafeCar::Table
2
+ class RowBuilder < Builder
3
+ option :object, default: -> { raise }
4
+
5
+ def href!
6
+ @href = @options.delete(:href) { true }
7
+
8
+ @href = @href.to_proc.(@object) if @href.respond_to?(:to_proc)
9
+ @href = nil if @href == false
10
+ end
11
+
12
+ def href = @href
13
+ def model = @object.class
14
+ def policy(o = @object) = @template.policy(o)
15
+
16
+ def value(method) = @object.try(method)
17
+ def show(...) = present(@object).show(...)
18
+
19
+ def cell(method, *flags, **options, &)
20
+ super
21
+ options[:href] = @object if options[:href] == true
22
+ call_procs!(options, @object)
23
+ ui.Cell(*flags, **options) { show(method, **options.slice(:blank), &) }
24
+ end
25
+
26
+ def timestamps(**options)
27
+ tip = show(:created_at).string&.then { "Created: #{_1}" }
28
+ cell(timestamp_attribute, :shrink, tip:, **options)
29
+ end
30
+
31
+ def controls(*args, **options)
32
+ ui.Cell(:shrink, :shy, :controls, *args) { present(@object).controls(*args, **options) }
33
+ end
34
+
35
+ def to_html
36
+ ui.Row { safe_join [ capture(self, &@block), @template.turbo_stream_from(@object) ] }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ module CafeCar
2
+ class TableBuilder < Table::Builder
3
+ def to_html
4
+ head = Table::HeadBuilder.new(@template, **@options, &@block).tap(&:to_s)
5
+ columns = head.fields.map(&:width).join(" ")
6
+ @template.ui.Table style: "grid-template-columns: #{columns}" do |table|
7
+ ~head
8
+ ~Table::BodyBuilder.new(@template, **@options, &@block)
9
+ ~Table::FootBuilder.new(@template, **@options, &@block)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module CafeCar
2
+ module TurboTagBuilder
3
+ def navigate(href)
4
+ action(:navigate, href)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module CafeCar
2
+ module UI
3
+ include Resolver
4
+
5
+ module_function
6
+
7
+ def component(name, **, &)
8
+ define_class(name, CafeCar[:Component], **, &)
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module CafeCar
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -0,0 +1,21 @@
1
+ module CafeCar::Visitors
2
+ module SQLite
3
+ def function(name, args, collector)
4
+ collector << "#{name}("
5
+ inject_join(args, collector, ", ") << ")"
6
+ end
7
+
8
+ def bool(value)
9
+ value ? Arel::Nodes::True.new : Arel::Nodes::False.new
10
+ end
11
+
12
+ def visit_Arel_Nodes_Regexp(o, collector)
13
+ function("regexp", [ o.right, o.left, bool(o.case_sensitive) ], collector)
14
+ end
15
+
16
+ def visit_Arel_Nodes_NotRegexp(o, collector)
17
+ collector << "NOT "
18
+ function("regexp", [ o.right, o.left, bool(o.case_sensitive) ], collector)
19
+ end
20
+ end
21
+ end
data/lib/cafe_car.rb CHANGED
@@ -1,181 +1,38 @@
1
+ require "cafe_car/core_ext"
2
+
1
3
  require "cafe_car/version"
2
- require "cafe_car/railtie"
4
+ require "cafe_car/engine"
5
+ require "cafe_car/resolver"
6
+ require "cafe_car/auto_resolver"
7
+ require "cafe_car/proc_helpers"
3
8
 
4
9
  module CafeCar
5
- extend ActiveSupport::Concern
6
-
7
- class_methods do
8
- def model(model)
9
- define_method(:record_model) { @record_model ||= model }
10
- end
11
- end
12
-
13
- included do
14
- helper_method :record_model, :record_name, :records_name, :record, :records, :record_policy
15
- rescue_from ActiveRecord::RecordInvalid, with: :render_invalid_record
16
- prepend_view_path 'app/views/cafe_car'
17
- end
18
-
19
- def index
20
- self.records = records.page(params[:page]).per(params[:per] || 100)
21
- self.records = filtered(records) if respond_to?(:filtered, true)
22
- self.records = paginated(records) if respond_to?(:paginated, true)
23
- self.records = sorted(records) if respond_to?(:sorted, true)
24
- self.records = records.includes(*includes) if respond_to?(:includes, true)
25
-
26
- validate_filter_scope(records)
27
-
28
- authorize records
29
- end
30
-
31
- def show
32
- self.record = record!
33
- authorize record
34
- end
35
-
36
- def new
37
- self.record = new_record
38
- authorize record
39
- end
10
+ include Resolver
11
+ extend AutoResolver
12
+ extend ProcHelpers
40
13
 
41
- def edit
42
- self.record = record!
43
- authorize record
14
+ class MissingAttributeError < StandardError
44
15
  end
45
16
 
46
- def create
47
- self.record = new_record
48
- authorize record
49
- record.assign_attributes(permitted_attributes(record))
50
-
51
- record.save!
52
- created
53
-
54
- respond_to do |f|
55
- f.js { created_js }
56
- f.html { created_redirect }
57
- f.json {}
58
- end
17
+ class AuthenticationFailed < StandardError
59
18
  end
60
19
 
61
- def update
62
- self.record = record!
63
- authorize record
64
-
65
- record.update!(permitted_attributes(record))
66
- updated
67
- respond_to do |f|
68
- f.js { updated_js }
69
- f.html { updated_redirect }
70
- f.json {}
71
- end
72
- end
20
+ def self.use_relative_model_naming? = true
73
21
 
74
- def destroy
75
- self.record = record!
76
- authorize record
77
-
78
- destroy_record
79
-
80
- destroyed
81
- respond_to do |f|
82
- f.js { destroyed_js }
83
- f.html { destroyed_redirect }
84
- f.json {}
85
- end
86
- end
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"
87
25
 
88
- def alter_tags
89
- self.record = record!
90
- authorize record
91
- # tag_list compares tags case-insensitively when adding, but not when removing
92
- remove = [*params[:remove]].map { |t| record.tag_list.find { |t2| t.casecmp(t2).zero? } }
93
- tags = record.tag_list.remove(*remove).add(*params[:add])
94
- if record.save
95
- render json: { tags: }
96
- else
97
- render json: { errors: record.errors.full_messages }, status: :bad_request
98
- end
99
- end
100
-
101
- private
102
-
103
- def new_record
104
- record_scope.new
105
- end
106
-
107
- def record!
108
- if record_model.respond_to?(:[])
109
- record_scope[params[:id]]
110
- else
111
- record_scope.find(params[:id])
112
- end
113
- end
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
114
29
 
115
- def record_scope
116
- record_model.all
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
117
37
  end
118
-
119
- def destroy_record
120
- record.destroy
121
- end
122
-
123
- def records!
124
- policy_scope(record_scope.all)
125
- end
126
-
127
- def records
128
- instance_variable_get("@#{records_name}") || (self.records = records!)
129
- end
130
-
131
- def records=(value)
132
- instance_variable_set("@#{records_name}", value)
133
- end
134
-
135
- def record
136
- instance_variable_get("@#{record_name}") || (self.record = record!)
137
- end
138
-
139
- def record=(value)
140
- instance_variable_set("@#{record_name}", value)
141
- end
142
-
143
- def record_model
144
- @record_model ||= self.class.name.gsub(/.*::|Controller$/, '').singularize.constantize
145
- end
146
-
147
- def record_policy = @policy ||= policy record
148
- def record_name = record_model.model_name.singular
149
- def records_name = record_model.model_name.plural
150
- def render_invalid_record = render record.persisted? ? 'edit' : 'new'
151
-
152
- def created_redirect = redirect_back fallback_location: [records_name.to_sym]
153
- def destroyed_redirect = redirect_to [records_name.to_sym]
154
-
155
- def updated_redirect
156
- return destroyed_redirect if record.destroyed?
157
- redirect_to record
158
- end
159
-
160
- def created_js
161
- render 'create'
162
- rescue ActionView::MissingTemplate
163
- created_redirect
164
- end
165
-
166
- def updated_js
167
- render 'update'
168
- rescue ActionView::MissingTemplate
169
- updated_redirect
170
- end
171
-
172
- def destroyed_js
173
- render 'destroy'
174
- rescue ActionView::MissingTemplate
175
- destroyed_redirect
176
- end
177
-
178
- def created; end
179
- def updated; end
180
- def destroyed; end
181
38
  end
@@ -0,0 +1,11 @@
1
+ Description:
2
+ Generates a new controller and routes for use with CafeCar.
3
+
4
+ Pass --skip-routes to generate only the controller.
5
+
6
+ Example:
7
+ `bin/rails generate cafe_car:controller admin/invoices`
8
+
9
+ This creates:
10
+ Controller: app/controllers/admin/invoices_controller.rb
11
+ Routes: resources :invoices, namespace: :admin
@@ -0,0 +1,26 @@
1
+ class CafeCar::ControllerGenerator < Rails::Generators::NamedBase
2
+ include CafeCar[:Generators]
3
+
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb."
7
+
8
+ check_class_collision suffix: "Controller"
9
+
10
+ def create_controller
11
+ template "controller.rb", File.join("app/controllers", class_path, "#{file_name}_controller.rb")
12
+ end
13
+
14
+ def add_resource_route
15
+ return if options.skip_routes?
16
+ route "#{resources} :#{file_name}", namespace: regular_class_path
17
+ end
18
+
19
+ private
20
+
21
+ def class_name = file_name.camelize
22
+ def plural? = file_name == plural_file_name
23
+ def plural_count = plural? ? 2 : 1
24
+ def resources = "resource".pluralize(plural_count)
25
+ def base_controller_name = "ApplicationController"
26
+ end
@@ -0,0 +1,5 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Controller < <%= base_controller_name %>
3
+ cafe_car
4
+ end
5
+ <% end -%>
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ bin/rails generate install Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,46 @@
1
+ class CafeCar::InstallGenerator < Rails::Generators::Base
2
+ include CafeCar::Generators
3
+
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ def install_deps
7
+ gem "bcrypt"
8
+ gem "paper_trail"
9
+ gem "factory_bot_rails"
10
+ gem "faker"
11
+ gem "rouge"
12
+
13
+ gem_group :development do
14
+ gem "hotwire-livereload"
15
+ gem "better_errors"
16
+ gem "binding_of_caller"
17
+ gem "chrome_devtools_rails"
18
+ gem "i18n-debug"
19
+ end
20
+ bundle_command "install"
21
+ end
22
+
23
+ def routes
24
+ route %(mount CafeCar::Engine => "/"), namespace: :admin
25
+ end
26
+
27
+ def install_pundit
28
+ template "application_policy.rb", "app/policies/application_policy.rb"
29
+ end
30
+
31
+ def install_js
32
+ inside "app/javascript" do
33
+ append_to_file "application.js", <<~JS
34
+ import "cafe_car"
35
+ import "trix"
36
+ import "@rails/actiontext"
37
+ JS
38
+ end
39
+ end
40
+
41
+ def install_controller
42
+ inject_into_class "app/controllers/application_controller.rb", "ApplicationController" do
43
+ " include CafeCar::Controller\n"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ class ApplicationPolicy < CafeCar::ApplicationPolicy
2
+ def admin? = Rails.env.development?
3
+
4
+ class Scope < Scope
5
+ def admin? = Rails.env.development?
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ Description:
2
+ Generates a Note resource that enables internal notes on any model with
3
+ `include Notable`. Permissions can be customized via the generated policy.
4
+
5
+ Example:
6
+ bin/rails generate cafe_car:notes
7
+
8
+ This will create:
9
+ app/models/note.rb
10
+ app/models/concerns/notable.rb
11
+ app/policies/note_policy.rb
12
+ app/controllers/admin/notes_controller.rb
@@ -0,0 +1,13 @@
1
+ class CafeCar::NotesGenerator < Rails::Generators::Base
2
+ include CafeCar::Generators
3
+
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ def create_notes
7
+ migration "create_notes"
8
+ template "note.rb", "app/models/note.rb"
9
+ template "notable.rb", "app/models/concerns/notable.rb"
10
+ generate "cafe_car:policy", "Note notable_id notable_type body", ("--force" if options.force?)
11
+ generate "cafe_car:controller", "admin/notes"
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # Generated via `rails cafe_car:notes`
2
+ class CreateNotes < ActiveRecord::Migration[8.0]
3
+ def change
4
+ create_table :notes do |t|
5
+ t.references :notable, polymorphic: true, null: false
6
+ t.references :author, null: false, foreign_key: {to_table: :users}
7
+ t.text :body
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ module Notable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ has_many :notes, as: :notable
6
+ end
7
+ end