cafe_car 0.1.2 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fdfdd44f9f4f564e6fe25b17a694b0dd6dce990efdc30990d0cef474a98cc718
4
- data.tar.gz: e483b2317f3a39b3b7c57b9c44bd20e3e766376f4889551bdc8ed199b8e6db52
3
+ metadata.gz: 7349873b72f79f05fd45bdb30e1a474b0d89d0b8352172378f6cecd239cc4f56
4
+ data.tar.gz: 1c2dcb971ecb7426bd168add0ffd46bff5fff204600e6f125aaa853db983feea
5
5
  SHA512:
6
- metadata.gz: daba4f7613586a3ff4f74e952800d3738965afafb81fbf1bfe82eec4b3ae5363c99d7aa879c1d3be438ff1ecdbf18d37d071307072487a1d2fb434dbfa3a6b62
7
- data.tar.gz: '051968fadd32602c6b8884443200f030aeae9bcecb731c602161b854f0222e8197b65d568eadce9e1c45175cc66b64fab899d74f5df367df856f7c39e29bc8eb'
6
+ metadata.gz: 7d9aebfaa9fec6e15149003739dab554bcd1e3279c66ef040fba72656b8d916c411cac39e05589e08e3b95b28a77311fac6875068e9a6f527f73c8a16d43fbe7
7
+ data.tar.gz: 037c7c894cd5c161e3c3544195074888d71166a6504e5e3115cd660372ac4038d793a1e78ab65713e760a49d019ff32717b73c99eba59a62fe935234705adfde
data/README.md CHANGED
@@ -1,9 +1,30 @@
1
- # CafeCar
1
+ <h1>
2
+ <img src="https://raw.githubusercontent.com/craft-concept/cafe_car/main/docs/images/logo.png"
3
+ alt="" width="40" valign="middle">
4
+ CafeCar
5
+ </h1>
2
6
 
3
7
  [![CI](https://img.shields.io/github/actions/workflow/status/craft-concept/cafe_car/ci.yml?branch=main&label=CI)](https://github.com/craft-concept/cafe_car/actions/workflows/ci.yml)
4
8
  [![Gem Version](https://img.shields.io/gem/v/cafe_car)](https://rubygems.org/gems/cafe_car)
5
9
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://opensource.org/licenses/MIT)
6
10
 
11
+ > 🚀 **[Live demo →](https://cafe-car-demo-production.up.railway.app)** — click straight
12
+ > into a real auto-generated admin (clients, invoices, articles, users, notes). No signup;
13
+ > the data resets periodically.
14
+
15
+ <p align="center">
16
+ <a href="https://cafe-car-demo-production.up.railway.app/admin/invoices">
17
+ <img src="https://raw.githubusercontent.com/craft-concept/cafe_car/main/docs/images/admin-invoices-index.png"
18
+ alt="CafeCar's auto-generated admin: an invoices index with sortable columns, currency formatting, association links, sender avatars, and pagination — all rendered straight from the model with no view code."
19
+ width="900">
20
+ </a>
21
+ </p>
22
+ <p align="center">
23
+ <em>A complete admin index — sortable columns, formatted values, association links, and
24
+ pagination — auto-generated from a model with one line of controller code.
25
+ <a href="https://cafe-car-demo-production.up.railway.app">Try the live demo →</a></em>
26
+ </p>
27
+
7
28
  CafeCar is a Rails engine that extends the MVC "view" layer to provide automatic
8
29
  CRUD UI generation with sensible defaults. Its philosophy is rooted in the idea
9
30
  that Rails should render _something_ that represents the CRUD operations of your
@@ -23,6 +44,10 @@ an application-wide or model-specific basis.
23
44
  - 📊 **Smart presenters** - Automatic type-aware display of your data
24
45
  - 🔍 **Advanced filtering** - Range queries, comparison operators, and
25
46
  association filters
47
+ - 🔎 **Keyword search** - Turnkey search box on every index, matching across a
48
+ model's text columns with zero per-model setup
49
+ - ⬇️ **CSV export** - One-click "Download CSV" of the current filtered, sorted
50
+ view on any index
26
51
  - 📄 **Pagination & sorting** - Kaminari integration with sortable columns
27
52
  - ⚡ **Hotwire ready** - Turbo Streams support out of the box
28
53
  - 📝 **Intelligent forms** - Auto-generated forms with smart field detection
@@ -86,7 +111,7 @@ $ rails db:migrate
86
111
  $ rails server
87
112
  ```
88
113
 
89
- Navigate to `/products` and you'll see a fully functional CRUD interface!
114
+ Navigate to `/products` and you'll see a working CRUD interface.
90
115
 
91
116
  ### Manual Setup
92
117
 
@@ -344,6 +369,18 @@ end
344
369
 
345
370
  CafeCar provides an enhanced form builder with smart field detection.
346
371
 
372
+ <p align="center">
373
+ <a href="https://cafe-car-demo-production.up.railway.app/admin/invoices/new">
374
+ <img src="https://raw.githubusercontent.com/craft-concept/cafe_car/main/docs/images/admin-invoice-form.png"
375
+ alt="CafeCar's auto-generated new-invoice form with a client association select, typed inputs, and nested has_many line items (add/remove rows)."
376
+ width="760">
377
+ </a>
378
+ </p>
379
+ <p align="center">
380
+ <em>An auto-generated form with a typed date field, an association select, and nested
381
+ <code>has_many</code> line items — add and remove rows inline.</em>
382
+ </p>
383
+
347
384
  **Basic forms:**
348
385
 
349
386
  ```erb
@@ -413,6 +450,51 @@ CafeCar provides advanced filtering with minimal configuration.
413
450
  /products?sort=category,-price # Multiple columns
414
451
  ```
415
452
 
453
+ **Keyword search:**
454
+
455
+ Every index ships with a search box — no configuration required. The `q`
456
+ parameter matches the term across the model's string/text columns
457
+ (case-insensitive, database-portable), and composes with the filters and sort
458
+ above:
459
+
460
+ ```
461
+ /products?q=widget
462
+ /products?q=widget&category=tools&sort=-price
463
+ ```
464
+
465
+ Define a `search` scope on the model to override the default with your own
466
+ logic (scoped columns, full-text, etc.):
467
+
468
+ ```ruby
469
+ class Product < ApplicationRecord
470
+ scope :search, ->(term) { where("sku = ?", term) }
471
+ end
472
+ ```
473
+
474
+ Columns hidden by Rails' parameter filter (passwords, tokens) are never
475
+ searched.
476
+
477
+ **CSV export:**
478
+
479
+ Every index also renders as CSV — append `.csv` or click the "Download CSV"
480
+ link. The export honors the current filters and sort and includes matching
481
+ records across the whole result set (not just the page on screen). Columns
482
+ mirror the JSON index — the same filtered attribute set — so protected columns
483
+ (passwords, tokens) and association foreign keys never appear in the file:
484
+
485
+ ```
486
+ /products.csv?category=tools&sort=-price
487
+ ```
488
+
489
+ To keep a large export from materializing an unbounded result set in memory, the
490
+ output is capped at `CafeCar.csv_export_row_limit` rows (default `10_000`). When a
491
+ download is truncated, CafeCar sets an `X-CafeCar-Truncated: true` response header
492
+ and logs a warning. Raise the limit in an initializer if your tables are larger:
493
+
494
+ ```ruby
495
+ CafeCar.csv_export_row_limit = 50_000
496
+ ```
497
+
416
498
  **In models:**
417
499
 
418
500
  ```ruby
@@ -1,5 +1,4 @@
1
1
  require "rouge"
2
- require "faker"
3
2
 
4
3
  module CafeCar
5
4
  class ExamplesController < const(:ApplicationController)
@@ -33,6 +33,34 @@ addEventListener("keydown", event => {
33
33
  }
34
34
  })
35
35
 
36
+ // Nested-attributes forms: add/remove repeatable `fields_for` rows. The add
37
+ // button clones the <template> (swapping the placeholder index for a unique
38
+ // one); the remove button drops unsaved rows from the DOM, or marks persisted
39
+ // rows for destruction by flipping their `_destroy` hidden field to "1".
40
+ addEventListener("click", event => {
41
+ let add = event.target.closest("[data-nested-add]")
42
+ if (add) {
43
+ let wrapper = add.closest("[data-nested-wrapper]")
44
+ let template = wrapper.querySelector("template[data-nested-template]")
45
+ let container = wrapper.querySelector("[data-nested-container]")
46
+ let html = template.innerHTML.replaceAll("CAFE_CAR_NEW_RECORD", Date.now())
47
+ container.insertAdjacentHTML("beforeend", html)
48
+ return
49
+ }
50
+
51
+ let remove = event.target.closest("[data-nested-remove]")
52
+ if (remove) {
53
+ let item = remove.closest("[data-nested-item]")
54
+ let destroy = item.querySelector("input[name*='[_destroy]']")
55
+ if (destroy) {
56
+ destroy.value = "1"
57
+ item.hidden = true
58
+ } else {
59
+ item.remove()
60
+ }
61
+ }
62
+ })
63
+
36
64
  function animationEnd({ target }) {
37
65
  if (target.matches(".remove")) target.remove()
38
66
  else if (target.matches(".popup")) {
@@ -1,3 +1,5 @@
1
+ = render "search"
2
+
1
3
  = render(view).presence or render("empty")
2
4
 
3
5
  .center
@@ -3,5 +3,7 @@
3
3
  = icon(:view_grid)
4
4
  = Button href: view_url(:table), tip: "Table View" do
5
5
  = icon(:table_rows)
6
+ = Button href: csv_url, tip: "Download CSV" do
7
+ = icon(:download)
6
8
 
7
9
  = link(model.build).new(class: ui.Button(:primary).class_name, params: dot_params) { icon(:plus) + _1}
@@ -0,0 +1,32 @@
1
+ - reflection = field.info.reflection
2
+ - if reflection&.collection?
3
+ - new_object = reflection.klass.new
4
+ - nested_opts = field.form.model.nested_attributes_options[field.method] || {}
5
+ - allow_destroy = nested_opts[:allow_destroy]
6
+ - plural = reflection.klass.model_name.plural
7
+ - item_partial = partial?("#{plural}/fields") ? "#{plural}/fields" : "fields"
8
+ = field.wrapper do
9
+ = field.label
10
+ = field.error
11
+ %div{data: {nested_wrapper: ""}}
12
+ %div{data: {nested_container: ""}}
13
+ = field.form.fields_for field.method do |f|
14
+ %div{data: {nested_item: ""}}
15
+ = render item_partial, f: f
16
+ - if allow_destroy
17
+ = f.hidden_field :_destroy, value: "0"
18
+ %button{type: "button", data: {nested_remove: ""}} Remove
19
+ %template{data: {nested_template: ""}}
20
+ %div{data: {nested_item: ""}}
21
+ = field.form.fields_for field.method, new_object, child_index: "CAFE_CAR_NEW_RECORD" do |f|
22
+ = render item_partial, f: f
23
+ - if allow_destroy
24
+ = f.hidden_field :_destroy, value: "0"
25
+ %button{type: "button", data: {nested_remove: ""}} Remove
26
+ %button{type: "button", data: {nested_add: ""}}= "+ #{field.info.label}"
27
+ - else
28
+ = field.wrapper do
29
+ = field.label
30
+ = field.error
31
+ = field.input
32
+ = field.hint
@@ -0,0 +1,6 @@
1
+ = form_with url: "", method: :get, class: "search" do |f|
2
+ - dot_params.each do |key, value|
3
+ = hidden_field_tag key, value
4
+ = hidden_field_tag :sort, params[:sort] if params[:sort].present?
5
+ = f.search_field :q, value: search_term, placeholder: t(:search, scope: :helpers, default: "Search")
6
+ = f.button "Search", name: nil
@@ -1,4 +1,4 @@
1
- = Alert :success, "Success alert. #{Faker::Lorem.paragraph}"
2
- = Alert :info, "Info alert. #{Faker::Lorem.paragraph}"
3
- = Alert :error, "Error alert. #{Faker::Lorem.paragraph}"
4
- = Alert :warning, "Warning alert. #{Faker::Lorem.sentence}"
1
+ = Alert :success, "Success alert. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
2
+ = Alert :info, "Info alert. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
3
+ = Alert :error, "Error alert. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
4
+ = Alert :warning, "Warning alert. Excepteur sint occaecat cupidatat non proident."
@@ -48,9 +48,9 @@ en:
48
48
  published_at: Published
49
49
 
50
50
  flashes:
51
- create_html: "%{Model} created!"
52
- update_html: "%{Model} updated!"
53
- destroy_html: "%{Model} deleted!"
51
+ create_html: "%{Model} created."
52
+ update_html: "%{Model} updated."
53
+ destroy_html: "%{Model} deleted."
54
54
 
55
55
  activerecord:
56
56
  models:
@@ -2,19 +2,26 @@ module CafeCar::Controller::Filtering
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
- helper_method :parsed_params, :dot_params, :filtered?
5
+ helper_method :parsed_params, :dot_params, :filtered?, :search_term
6
6
  end
7
7
 
8
8
  private
9
9
 
10
10
  def filtered(scope)
11
- scope.query(parsed_params[""])
11
+ scope.query([ parsed_params[""], search_term ].compact_blank)
12
12
  end
13
13
 
14
14
  def filtered?
15
- parsed_params[""].present?
15
+ parsed_params[""].present? || search_term.present?
16
16
  end
17
17
 
18
+ # Keyword term from the index search box. Read raw (not through ParamParser) so a
19
+ # term keeps its literal text, then funneled into the query DSL as a bare String
20
+ # that routes to `QueryBuilder#search!` alongside the dot-filters. Only a String
21
+ # counts — a crafted `?q[x]=y` (Hash) or `?q[]=a` (Array) is ignored, not searched
22
+ # (otherwise the non-String would reach the query DSL and raise a 500).
23
+ def search_term = (params[:q] if params[:q].is_a?(String)).presence
24
+
18
25
  def dot_params
19
26
  request.params.slice(*request.params.keys.grep(/^\./))
20
27
  end
@@ -32,7 +32,7 @@ module CafeCar
32
32
 
33
33
  append_cafe_car_views
34
34
 
35
- respond_to :json, :html, :turbo_stream
35
+ respond_to :json, :html, :turbo_stream, :csv
36
36
 
37
37
  before_action :set_current_attributes
38
38
 
@@ -41,6 +41,8 @@ module CafeCar
41
41
  before_action :find_objects, only: _only.(%i[index])
42
42
  before_action :assign_attributes, only: _only.(%i[create update])
43
43
  before_action :authorize!
44
+
45
+ after_action :verify_authorized, :verify_policy_scoped
44
46
  end
45
47
 
46
48
  def append_cafe_car_views
@@ -77,8 +79,6 @@ module CafeCar
77
79
 
78
80
  helper Helpers
79
81
  delegate :present, :href_for, :namespace, to: :helpers
80
-
81
- after_action :verify_authorized, :verify_policy_scoped
82
82
  end
83
83
 
84
84
  def index = respond_with objects
@@ -122,6 +122,7 @@ module CafeCar
122
122
  end
123
123
 
124
124
  def paginated(scope, page: params[:page], per: params[:per])
125
+ return scope if request.format.csv? # CSV exports the full filtered+sorted set
125
126
  scope.page(page).per(per)
126
127
  end
127
128
 
@@ -79,6 +79,50 @@ module CafeCar
79
79
  config.responders.flash_keys = [ :success, :error ]
80
80
  end
81
81
 
82
+ initializer "cafe_car.csv_renderer" do
83
+ require "csv"
84
+
85
+ # CSV export of an index's filtered+sorted collection. Columns mirror the
86
+ # JSON renderer's policy-respecting basis (`[:id] | displayable_attributes`)
87
+ # so exports never leak attributes the policy hides, narrowed to scalar DB
88
+ # columns. Associations are out of scope for v1 (only scalar values).
89
+ ActionController::Renderers.add :csv do |collection, options|
90
+ klass = collection.respond_to?(:klass) ? collection.klass : collection.class
91
+ basis = options[:only] || [ :id ] | policy(klass.new).displayable_attributes
92
+ columns = basis & klass.column_names.map(&:to_sym)
93
+ text = columns.select { klass.columns_hash[_1.to_s]&.type.in?(%i[string text]) }
94
+
95
+ # Neutralize CSV formula injection: a text value a spreadsheet would treat as
96
+ # a formula (leading = + - @, tab, CR) is prefixed with a quote. Guard only
97
+ # text columns so numeric values (e.g. negative amounts) aren't mangled.
98
+ cell = ->(col, value) do
99
+ text.include?(col) && value.to_s.match?(/\A[=+\-@\t\r]/) ? "'#{value}" : value
100
+ end
101
+
102
+ # Bound the export: cap rows so a huge table can't materialize unbounded
103
+ # in memory. We deliberately limit (not `find_each`) to preserve the
104
+ # filtered+sorted order the export exists to mirror. A truncated export
105
+ # is signalled out-of-band so the data columns stay aligned.
106
+ cap = CafeCar.csv_export_row_limit
107
+ scope = collection.respond_to?(:limit) ? collection.limit(cap) : Array(collection).first(cap)
108
+ rows = 0
109
+
110
+ data = CSV.generate do |csv|
111
+ csv << columns.map { klass.human_attribute_name(_1) }
112
+ scope.each { |record| csv << columns.map { cell.(_1, record.public_send(_1)) }; rows += 1 }
113
+ end
114
+
115
+ if rows == cap
116
+ Rails.logger.warn("CafeCar CSV export of #{klass.name} truncated at #{cap} rows")
117
+ headers["X-CafeCar-Truncated"] = "true"
118
+ end
119
+
120
+ self.content_type ||= Mime[:csv]
121
+ headers["Content-Disposition"] ||= %(attachment; filename="#{controller_name}.csv")
122
+ data
123
+ end
124
+ end
125
+
82
126
  initializer "cafe_car.field_with_errors" do
83
127
  ActionView::Base.field_error_proc = proc { _1.html_safe }
84
128
  end
@@ -59,7 +59,7 @@ module CafeCar
59
59
 
60
60
  def nested_attributes_type
61
61
  key = @method.to_s.chomp("_attributes").to_sym
62
- model.nested_attributes_options.key?(key) && :nested
62
+ :nested if model.nested_attributes_options.key?(key)
63
63
  end
64
64
 
65
65
  def polymorphic_methods = [ reflection.foreign_type, reflection.foreign_key ]
@@ -85,8 +85,8 @@ module CafeCar
85
85
 
86
86
  def type
87
87
  return if @method.nil?
88
- @type ||= reflection_type || attribute_type || digest_type || attachment_type ||
89
- default_type || nested_attributes_type ||
88
+ @type ||= nested_attributes_type || reflection_type || attribute_type || digest_type ||
89
+ attachment_type || default_type ||
90
90
  raise(NoMethodError.new "Can't find attribute :#{@method} on #{model_name}", @method)
91
91
  end
92
92
 
@@ -1,5 +1,6 @@
1
1
  require "rails/generators/active_record/migration"
2
2
  require "rails/generators/bundle_helper"
3
+ require "shellwords"
3
4
 
4
5
  module CafeCar::Generators
5
6
  extend ActiveSupport::Concern
@@ -8,6 +9,18 @@ module CafeCar::Generators
8
9
 
9
10
  private
10
11
 
12
+ # Delegate to another generator inline, anchored to THIS generator's
13
+ # destination_root. Rails' built-in `generate` action recomputes the
14
+ # destination from Rails::Command.root, which leaks writes into the engine
15
+ # repo (when a contributor runs the generator there) or escapes the test
16
+ # destination. Invoking the generator directly keeps every sub-generator
17
+ # writing where the parent does.
18
+ def generate(what, *args)
19
+ args.extract_options! # drop trailing option hashes (e.g. inline:); always inline here
20
+ what, *args = Shellwords.split("#{what} #{args.map(&:to_s).join(' ')}")
21
+ Rails::Generators.invoke(what, args, behavior: behavior, destination_root: destination_root)
22
+ end
23
+
11
24
  def migration(name, ...) = migration_template("#{name}.rb", "db/migrate/#{name}.rb", ...)
12
25
 
13
26
  def model(name, ...)
@@ -77,6 +77,10 @@ module CafeCar
77
77
  url_for(params)
78
78
  end
79
79
 
80
+ # Current index URL as `.csv`, carrying the on-screen filter + sort params so
81
+ # the export matches the view. Pagination is dropped (CSV exports the full set).
82
+ def csv_url = url_for(request.params.except("page", "per").merge(format: :csv))
83
+
80
84
  def context(name = nil, &)
81
85
  @context ||= []
82
86
 
@@ -154,9 +154,8 @@ module CafeCar
154
154
  end
155
155
 
156
156
  def search!(term)
157
- @scope.search!(term) if @scope.respond_to?(:search!)
158
157
  @scope.query!("body~": term) if @scope < ::ActionText::RichText
159
- update! { _1.search(term) }
158
+ update! { _1.respond_to?(:search) ? _1.search(term) : _1.default_search(term) }
160
159
  end
161
160
 
162
161
  def query!(params = nil)
@@ -12,6 +12,25 @@ module CafeCar::Queryable
12
12
  def query(params) = query_builder.query(params).scope
13
13
  def query!(params) = query_builder.query!(params).scope
14
14
 
15
+ # Turnkey keyword search: case-insensitive match of `term` across the model's
16
+ # string/text columns. Uses Arel `#matches` so it stays DB-portable (ILIKE on
17
+ # Postgres, LIKE on SQLite/MySQL). Columns the parameter filter hides (passwords,
18
+ # tokens, ...) are skipped, mirroring the policy's displayable guarantee. Hosts
19
+ # that declare their own `scope :search` take precedence (see QueryBuilder#search!).
20
+ def default_search(term)
21
+ columns = searchable_columns
22
+ return none if columns.empty?
23
+ pattern = "%#{sanitize_sql_like(term.to_s)}%"
24
+ where(columns.map { arel_table[_1].matches(pattern) }.reduce(:or))
25
+ end
26
+
27
+ def searchable_columns
28
+ columns_hash.values
29
+ .select { _1.type.in?(%i[string text]) }
30
+ .map(&:name)
31
+ .reject { inspection_filter.filter_param(_1, nil).present? }
32
+ end
33
+
15
34
  def query_builder
16
35
  CafeCar::QueryBuilder.new(all)
17
36
  end
@@ -1,3 +1,3 @@
1
1
  module CafeCar
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/cafe_car.rb CHANGED
@@ -23,6 +23,10 @@ module CafeCar
23
23
  # user model can override this (e.g. `CafeCar.user_class_name = "Account"`).
24
24
  mattr_accessor :user_class_name, default: "User"
25
25
 
26
+ # Maximum number of rows a CSV export will emit. Bounds the memory/latency cost
27
+ # of exporting a large table; truncated exports signal `X-CafeCar-Truncated`.
28
+ mattr_accessor :csv_export_row_limit, default: 10_000
29
+
26
30
  # The host's user model, resolved lazily so the constant need not exist at
27
31
  # boot. Used by CafeCar::Session for authentication.
28
32
  def self.user_class = user_class_name.to_s.constantize
@@ -13,9 +13,14 @@ class CafeCar::PolicyGenerator < Rails::Generators::NamedBase
13
13
 
14
14
  private
15
15
 
16
+ # Use the leaf name inside the template; module_namespacing already emits the
17
+ # surrounding `module Admin`, so the full path would double-namespace it
18
+ # (`module Admin; class Admin::PaymentPolicy`). Mirrors the controller generator.
19
+ def class_name = file_name.camelize
20
+
16
21
  def base_policy_name = "ApplicationPolicy"
17
22
 
18
- def model_class = class_name.classify.safe_constantize
23
+ def model_class = file_path.classify.safe_constantize
19
24
 
20
25
  def model = @model ||= CafeCar[:ModelInfo].new(model_class)
21
26
 
@@ -2,20 +2,21 @@ require "rails/generators/resource_helpers"
2
2
 
3
3
  class CafeCar::ResourceGenerator < Rails::Generators::NamedBase
4
4
  include Rails::Generators::ResourceHelpers
5
+ include CafeCar::Generators
5
6
 
6
7
  source_root File.expand_path("templates", __dir__)
7
8
  argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
8
9
 
9
10
  def create_model
10
- generate "model", file_path, *attributes, force, inline: true
11
+ generate "model", file_path, *attributes, force
11
12
  end
12
13
 
13
14
  def create_controller
14
- generate "cafe_car:controller", controller_name, force, inline: true
15
+ generate "cafe_car:controller", controller_name, force
15
16
  end
16
17
 
17
18
  def create_policy
18
- generate "cafe_car:policy", file_path, force, inline: true
19
+ generate "cafe_car:policy", file_path, force
19
20
  end
20
21
 
21
22
  private
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cafe_car
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Peterson
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-06-30 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rails
@@ -177,20 +178,6 @@ dependencies:
177
178
  - - ">="
178
179
  - !ruby/object:Gem::Version
179
180
  version: '0.10'
180
- - !ruby/object:Gem::Dependency
181
- name: web-console
182
- requirement: !ruby/object:Gem::Requirement
183
- requirements:
184
- - - ">="
185
- - !ruby/object:Gem::Version
186
- version: '4.0'
187
- type: :runtime
188
- prerelease: false
189
- version_requirements: !ruby/object:Gem::Requirement
190
- requirements:
191
- - - ">="
192
- - !ruby/object:Gem::Version
193
- version: '4.0'
194
181
  - !ruby/object:Gem::Dependency
195
182
  name: rouge
196
183
  requirement: !ruby/object:Gem::Requirement
@@ -205,20 +192,6 @@ dependencies:
205
192
  - - ">="
206
193
  - !ruby/object:Gem::Version
207
194
  version: '4.0'
208
- - !ruby/object:Gem::Dependency
209
- name: faker
210
- requirement: !ruby/object:Gem::Requirement
211
- requirements:
212
- - - ">="
213
- - !ruby/object:Gem::Version
214
- version: '3.0'
215
- type: :runtime
216
- prerelease: false
217
- version_requirements: !ruby/object:Gem::Requirement
218
- requirements:
219
- - - ">="
220
- - !ruby/object:Gem::Version
221
- version: '3.0'
222
195
  description: CafeCar is a Rails engine that extends the view layer to render complete
223
196
  index, show, new, and edit interfaces straight from your models, with no boilerplate
224
197
  required. Sensible defaults cover authorization, presenters, filtering, sorting,
@@ -332,8 +305,10 @@ files:
332
305
  - app/views/application/_index_actions.html.haml
333
306
  - app/views/application/_navigation.html.haml
334
307
  - app/views/application/_navigation_links.html.haml
308
+ - app/views/application/_nested_field.html.haml
335
309
  - app/views/application/_notes.html.haml
336
310
  - app/views/application/_popup.html.haml
311
+ - app/views/application/_search.html.haml
337
312
  - app/views/application/_show.html.haml
338
313
  - app/views/application/_submit.html.haml
339
314
  - app/views/application/_table.html.haml
@@ -475,6 +450,7 @@ metadata:
475
450
  changelog_uri: https://github.com/craft-concept/cafe_car/blob/main/CHANGELOG.md
476
451
  bug_tracker_uri: https://github.com/craft-concept/cafe_car/issues
477
452
  rubygems_mfa_required: 'true'
453
+ post_install_message:
478
454
  rdoc_options: []
479
455
  require_paths:
480
456
  - lib
@@ -489,7 +465,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
489
465
  - !ruby/object:Gem::Version
490
466
  version: '0'
491
467
  requirements: []
492
- rubygems_version: 3.6.7
468
+ rubygems_version: 3.5.22
469
+ signing_key:
493
470
  specification_version: 4
494
471
  summary: Auto-generate CRUD admin UI for your Rails models, with sensible overridable
495
472
  defaults.