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.
- checksums.yaml +4 -4
- data/README.md +682 -8
- data/Rakefile +21 -0
- data/app/assets/fonts/Lexend.css +7 -0
- data/app/assets/fonts/Lexend.ttf +0 -0
- data/app/assets/images/noise.svg +16 -0
- data/app/assets/stylesheets/actiontext.css +31 -0
- data/app/assets/stylesheets/application.css +1 -0
- data/app/assets/stylesheets/cafe_car/code/base16-dark.css +89 -0
- data/app/assets/stylesheets/cafe_car/code/base16-light.css +90 -0
- data/app/assets/stylesheets/cafe_car/pagination.css +5 -0
- data/app/assets/stylesheets/cafe_car/themes/cool.css +32 -0
- data/app/assets/stylesheets/cafe_car/themes/cool2.css +31 -0
- data/app/assets/stylesheets/cafe_car/themes/defaults.css +60 -0
- data/app/assets/stylesheets/cafe_car/themes/warm-dark.css +29 -0
- data/app/assets/stylesheets/cafe_car/themes/warm.css +24 -0
- data/app/assets/stylesheets/cafe_car/tooltips.css +20 -0
- data/app/assets/stylesheets/cafe_car/trix.css +56 -0
- data/app/assets/stylesheets/cafe_car/utility.css +63 -0
- data/app/assets/stylesheets/cafe_car.css +96 -0
- data/app/assets/stylesheets/iconoir.css +22 -0
- data/app/assets/stylesheets/ui/Alert.css +25 -0
- data/app/assets/stylesheets/ui/Article.css +11 -0
- data/app/assets/stylesheets/ui/Button.css +42 -0
- data/app/assets/stylesheets/ui/Card.css +74 -0
- data/app/assets/stylesheets/ui/Chat.css +33 -0
- data/app/assets/stylesheets/ui/Close.css +11 -0
- data/app/assets/stylesheets/ui/Code.css +4 -0
- data/app/assets/stylesheets/ui/Controls.css +16 -0
- data/app/assets/stylesheets/ui/Error.css +3 -0
- data/app/assets/stylesheets/ui/Example.css +45 -0
- data/app/assets/stylesheets/ui/Field.css +31 -0
- data/app/assets/stylesheets/ui/Grid.css +6 -0
- data/app/assets/stylesheets/ui/Group.css +16 -0
- data/app/assets/stylesheets/ui/Icon.css +27 -0
- data/app/assets/stylesheets/ui/Image.css +14 -0
- data/app/assets/stylesheets/ui/InfoCircle.css +11 -0
- data/app/assets/stylesheets/ui/Input.css +36 -0
- data/app/assets/stylesheets/ui/Layout.css +100 -0
- data/app/assets/stylesheets/ui/Menu.css +38 -0
- data/app/assets/stylesheets/ui/Modal.css +26 -0
- data/app/assets/stylesheets/ui/Navigation.css +37 -0
- data/app/assets/stylesheets/ui/Page.css +105 -0
- data/app/assets/stylesheets/ui/Row.css +9 -0
- data/app/assets/stylesheets/ui/Table.css +101 -0
- data/app/assets/stylesheets/ui/components.css +24 -0
- data/app/controllers/cafe_car/application_controller.rb +9 -0
- data/app/controllers/cafe_car/examples_controller.rb +22 -0
- data/app/controllers/cafe_car/sessions_controller.rb +30 -0
- data/app/controllers/concerns/cafe_car/authentication.rb +61 -0
- data/app/javascript/application.js +5 -0
- data/app/javascript/cafe_car.js +174 -0
- data/app/models/cafe_car/session.rb +18 -0
- data/app/policies/cafe_car/application_policy.rb +42 -0
- data/app/policies/cafe_car/session_policy.rb +19 -0
- data/app/presenters/cafe_car/action_text/rich_text_presenter.rb +7 -0
- data/app/presenters/cafe_car/active_record/base_presenter.rb +6 -0
- data/app/presenters/cafe_car/active_record/relation_presenter.rb +17 -0
- data/app/presenters/cafe_car/active_storage/attached/one_presenter.rb +9 -0
- data/app/presenters/cafe_car/active_storage/attachment_presenter.rb +18 -0
- data/app/presenters/cafe_car/basic_object_presenter.rb +5 -0
- data/app/presenters/cafe_car/code_presenter.rb +18 -0
- data/app/presenters/cafe_car/currency_presenter.rb +5 -0
- data/app/presenters/cafe_car/date_and_time/compatibility_presenter.rb +6 -0
- data/app/presenters/cafe_car/date_presenter.rb +5 -0
- data/app/presenters/cafe_car/date_time_presenter.rb +11 -0
- data/app/presenters/cafe_car/enumerable_presenter.rb +13 -0
- data/app/presenters/cafe_car/false_class_presenter.rb +5 -0
- data/app/presenters/cafe_car/hash_presenter.rb +6 -0
- data/app/presenters/cafe_car/nil_class_presenter.rb +13 -0
- data/app/presenters/cafe_car/presenter.rb +157 -0
- data/app/presenters/cafe_car/range_presenter.rb +16 -0
- data/app/presenters/cafe_car/record_presenter.rb +5 -0
- data/app/presenters/cafe_car/string_presenter.rb +20 -0
- data/app/presenters/cafe_car/symbol_presenter.rb +5 -0
- data/app/presenters/cafe_car/true_class_presenter.rb +5 -0
- data/app/ui/cafe_car/ui/button.rb +9 -0
- data/app/ui/cafe_car/ui/card.rb +18 -0
- data/app/ui/cafe_car/ui/field.rb +11 -0
- data/app/ui/cafe_car/ui/grid.rb +30 -0
- data/app/ui/cafe_car/ui/layout.rb +7 -0
- data/app/ui/cafe_car/ui/page.rb +14 -0
- data/app/views/application/_actions.html.haml +1 -0
- data/app/views/application/_alerts.html.haml +2 -0
- data/app/views/application/_body.html.haml +7 -0
- data/app/views/application/_controls.html.haml +12 -0
- data/app/views/application/_debug.html.haml +18 -0
- data/app/views/application/_empty.html.haml +1 -0
- data/app/views/application/_errors.html.haml +4 -0
- data/app/views/application/_field.html.haml +5 -0
- data/app/views/application/_fields.html.haml +1 -0
- data/app/views/application/_filters.html.haml +8 -0
- data/app/views/application/_form.html.haml +6 -0
- data/app/views/application/_grid.html.haml +3 -0
- data/app/views/application/_grid_item.html.haml +1 -0
- data/app/views/application/_head.html.haml +17 -0
- data/app/views/application/_index.html.haml +8 -0
- data/app/views/application/_index_actions.html.haml +7 -0
- data/app/views/application/_navigation.html.haml +9 -0
- data/app/views/application/_navigation_links.html.haml +5 -0
- data/app/views/application/_notes.html.haml +10 -0
- data/app/views/application/_popup.html.haml +7 -0
- data/app/views/application/_show.html.haml +9 -0
- data/app/views/application/_submit.html.haml +1 -0
- data/app/views/application/_table.html.haml +6 -0
- data/app/views/cafe_car/application/create.turbo_stream.haml +2 -0
- data/app/views/cafe_car/application/destroy.turbo_stream.haml +1 -0
- data/app/views/cafe_car/application/edit.html.haml +18 -0
- data/app/views/cafe_car/application/edit.turbo_stream.haml +7 -0
- data/app/views/cafe_car/application/index.html.haml +15 -0
- data/app/views/cafe_car/application/new.html.haml +8 -0
- data/app/views/cafe_car/application/new.turbo_stream.haml +8 -0
- data/app/views/cafe_car/application/show.html.haml +36 -0
- data/app/views/cafe_car/application/update.turbo_stream.haml +2 -0
- data/app/views/cafe_car/examples/_example.html.haml +12 -0
- data/app/views/cafe_car/examples/_index.html.haml +16 -0
- data/app/views/cafe_car/examples/_navigation_links.html.haml +2 -0
- data/app/views/cafe_car/examples/ui/_alert.html.haml +4 -0
- data/app/views/cafe_car/examples/ui/_button.html.haml +3 -0
- data/app/views/cafe_car/examples/ui/_card.html.haml +6 -0
- data/app/views/cafe_car/examples/ui/_chat.html.haml +3 -0
- data/app/views/cafe_car/examples/ui/_controls.html.haml +3 -0
- data/app/views/cafe_car/examples/ui/_error.html.haml +1 -0
- data/app/views/cafe_car/examples/ui/_field.html.haml +9 -0
- data/app/views/cafe_car/examples/ui/_grid.html.haml +11 -0
- data/app/views/cafe_car/examples/ui/_group.html.haml +21 -0
- data/app/views/cafe_car/examples/ui/_info_circle.html.haml +1 -0
- data/app/views/cafe_car/examples/ui/_menu.html.haml +5 -0
- data/app/views/cafe_car/examples/ui/_modal.html.haml +4 -0
- data/app/views/cafe_car/examples/ui/_navigation.html.haml +4 -0
- data/app/views/cafe_car/examples/ui/_page.html.haml +4 -0
- data/app/views/cafe_car/examples/ui/_table.html.haml +13 -0
- data/app/views/cafe_car/layouts/mailer.html.haml +8 -0
- data/app/views/cafe_car/layouts/mailer.text.erb +1 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/app/views/layouts/application.html.haml +4 -0
- data/app/views/layouts/mailer.html.erb +13 -0
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/notes/_fields.html.haml +1 -0
- data/app/views/passwords_mailer/reset.html.haml +5 -0
- data/app/views/passwords_mailer/reset.text.erb +4 -0
- data/app/views/ui/_card.html.haml +8 -0
- data/app/views/ui/_field.html.haml +1 -0
- data/app/views/ui/_modal_close.html.haml +1 -0
- data/app/views/ui/_page.html.haml +7 -0
- data/config/brakeman.ignore +77 -0
- data/config/importmap.rb +12 -0
- data/config/locales/en.yml +63 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20251005220017_create_slugs.rb +13 -0
- data/lib/cafe_car/active_record.rb +21 -0
- data/lib/cafe_car/application_responder.rb +17 -0
- data/lib/cafe_car/attributes.rb +23 -0
- data/lib/cafe_car/auto_resolver.rb +49 -0
- data/lib/cafe_car/caching.rb +20 -0
- data/lib/cafe_car/component.rb +155 -0
- data/lib/cafe_car/context.rb +17 -0
- data/lib/cafe_car/controller/filtering.rb +30 -0
- data/lib/cafe_car/controller.rb +218 -0
- data/lib/cafe_car/core_ext/array.rb +24 -0
- data/lib/cafe_car/core_ext/hash.rb +15 -0
- data/lib/cafe_car/core_ext/module.rb +15 -0
- data/lib/cafe_car/core_ext.rb +5 -0
- data/lib/cafe_car/current.rb +9 -0
- data/lib/cafe_car/engine.rb +107 -0
- data/lib/cafe_car/field_builder.rb +44 -0
- data/lib/cafe_car/field_info.rb +144 -0
- data/lib/cafe_car/fields.rb +21 -0
- data/lib/cafe_car/filter/field_builder.rb +4 -0
- data/lib/cafe_car/filter/field_info.rb +22 -0
- data/lib/cafe_car/filter/form_builder.rb +21 -0
- data/lib/cafe_car/filter.rb +5 -0
- data/lib/cafe_car/filter_builder.rb +20 -0
- data/lib/cafe_car/form_builder.rb +105 -0
- data/lib/cafe_car/generators.rb +30 -0
- data/lib/cafe_car/helpers.rb +178 -0
- data/lib/cafe_car/href_builder.rb +97 -0
- data/lib/cafe_car/informable.rb +9 -0
- data/lib/cafe_car/input_builder.rb +25 -0
- data/lib/cafe_car/inputs/association_builder.rb +6 -0
- data/lib/cafe_car/inputs/base_input.rb +19 -0
- data/lib/cafe_car/inputs/belongs_to_builder.rb +6 -0
- data/lib/cafe_car/inputs/password_input.rb +7 -0
- data/lib/cafe_car/inputs/string_input.rb +7 -0
- data/lib/cafe_car/link_builder.rb +65 -0
- data/lib/cafe_car/model.rb +23 -0
- data/lib/cafe_car/model_info.rb +24 -0
- data/lib/cafe_car/name_patch.rb +17 -0
- data/lib/cafe_car/navigation.rb +76 -0
- data/lib/cafe_car/option_helpers.rb +53 -0
- data/lib/cafe_car/param_parser.rb +45 -0
- data/lib/cafe_car/pluralization.rb +15 -0
- data/lib/cafe_car/policy.rb +77 -0
- data/lib/cafe_car/proc_helpers.rb +13 -0
- data/lib/cafe_car/query_builder.rb +186 -0
- data/lib/cafe_car/queryable.rb +29 -0
- data/lib/cafe_car/resolver.rb +27 -0
- data/lib/cafe_car/routing.rb +17 -0
- data/lib/cafe_car/table/body_builder.rb +12 -0
- data/lib/cafe_car/table/builder.rb +52 -0
- data/lib/cafe_car/table/foot_builder.rb +14 -0
- data/lib/cafe_car/table/head_builder.rb +26 -0
- data/lib/cafe_car/table/label_builder.rb +48 -0
- data/lib/cafe_car/table/objects_builder.rb +8 -0
- data/lib/cafe_car/table/row_builder.rb +39 -0
- data/lib/cafe_car/table_builder.rb +13 -0
- data/lib/cafe_car/turbo_tag_builder.rb +7 -0
- data/lib/cafe_car/ui.rb +11 -0
- data/lib/cafe_car/version.rb +1 -1
- data/lib/cafe_car/visitors.rb +21 -0
- data/lib/cafe_car.rb +25 -168
- data/lib/generators/cafe_car/controller/USAGE +11 -0
- data/lib/generators/cafe_car/controller/controller_generator.rb +26 -0
- data/lib/generators/cafe_car/controller/templates/controller.rb.tt +5 -0
- data/lib/generators/cafe_car/install/USAGE +8 -0
- data/lib/generators/cafe_car/install/install_generator.rb +46 -0
- data/lib/generators/cafe_car/install/templates/application_policy.rb.tt +7 -0
- data/lib/generators/cafe_car/notes/USAGE +12 -0
- data/lib/generators/cafe_car/notes/notes_generator.rb +13 -0
- data/lib/generators/cafe_car/notes/templates/create_notes.rb.tt +12 -0
- data/lib/generators/cafe_car/notes/templates/notable.rb.tt +7 -0
- data/lib/generators/cafe_car/notes/templates/note.rb.tt +6 -0
- data/lib/generators/cafe_car/policy/USAGE +8 -0
- data/lib/generators/cafe_car/policy/policy_generator.rb +39 -0
- data/lib/generators/cafe_car/policy/templates/policy.rb.tt +20 -0
- data/lib/generators/cafe_car/resource/USAGE +13 -0
- data/lib/generators/cafe_car/resource/resource_generator.rb +32 -0
- data/lib/generators/cafe_car/sessions/USAGE +17 -0
- data/lib/generators/cafe_car/sessions/sessions_generator.rb +29 -0
- data/lib/generators/cafe_car/sessions/templates/create_sessions.rb.tt +12 -0
- data/lib/tasks/holdco_tasks.rake +532 -0
- data/lib/tasks/templates/tasks_header.md +37 -0
- metadata +444 -21
- data/app/views/cafe_car/application/_fields.html.erb +0 -7
- data/app/views/cafe_car/application/_filters.html.erb +0 -0
- data/app/views/cafe_car/application/_form.html.erb +0 -22
- data/lib/cafe_car/railtie.rb +0 -4
- /data/app/views/{cafe_car/application/_actions.html.erb → application/_aside.html.haml} +0 -0
- /data/app/views/{cafe_car/application/_aside.html.erb → application/_footer.html.haml} +0 -0
- /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,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 ], " ".html_safe)
|
|
46
|
+
def to_html = @template.link_to_unless(!sortable?, content, href)
|
|
47
|
+
end
|
|
48
|
+
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
|
data/lib/cafe_car/ui.rb
ADDED
data/lib/cafe_car/version.rb
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
42
|
-
self.record = record!
|
|
43
|
-
authorize record
|
|
14
|
+
class MissingAttributeError < StandardError
|
|
44
15
|
end
|
|
45
16
|
|
|
46
|
-
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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,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,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
|