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,19 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
module Inputs
|
|
3
|
+
class BaseInput
|
|
4
|
+
attr_accessor :options
|
|
5
|
+
|
|
6
|
+
def initialize(template:, **options)
|
|
7
|
+
@template = template
|
|
8
|
+
@options = options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def tag = :input
|
|
12
|
+
def type = :text
|
|
13
|
+
|
|
14
|
+
def to_html
|
|
15
|
+
@template.Input(tag:, type:, **options)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class LinkBuilder
|
|
3
|
+
attr_reader :object
|
|
4
|
+
|
|
5
|
+
delegate :capture, :link_to, :link_to_unless, :current_page?, :href_for, to: :@template
|
|
6
|
+
delegate :model, :policy, to: :p
|
|
7
|
+
|
|
8
|
+
def initialize(template, object, namespace: template.namespace)
|
|
9
|
+
@template = template
|
|
10
|
+
@object = object
|
|
11
|
+
@namespace = namespace
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def p = @template.present(@object)
|
|
15
|
+
def can?(action) = policy.public_send("#{action}?")
|
|
16
|
+
def cant?(action) = !can?(action) && disabled(action, :policy)
|
|
17
|
+
|
|
18
|
+
def i18n(*, scope: nil, **) = p.i18n(*, scope: [ :controls, *scope ], **)
|
|
19
|
+
|
|
20
|
+
def confirm(key) = i18n(key, scope: :confirm)
|
|
21
|
+
def disabled(action, reason) = i18n(action, scope: [ :disabled, reason ])
|
|
22
|
+
|
|
23
|
+
def turbo!(opts)
|
|
24
|
+
opts.replace({
|
|
25
|
+
data: {
|
|
26
|
+
turbo_stream: true,
|
|
27
|
+
turbo_method: opts.delete(:method),
|
|
28
|
+
turbo_confirm: opts.delete(:confirm)
|
|
29
|
+
}
|
|
30
|
+
}.deep_merge(opts))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def link(action, target, label = i18n(action), disabled: false, hide: false, params: nil, **opts, &)
|
|
34
|
+
params ||= {}
|
|
35
|
+
disabled ||= cant?(action)
|
|
36
|
+
return if disabled and hide
|
|
37
|
+
|
|
38
|
+
href = href_for(*target, action:, namespace: @namespace, **params)
|
|
39
|
+
current = current_page?(href, check_parameters: true)
|
|
40
|
+
in_link = @template.context?(:a)
|
|
41
|
+
content = block_given? ? capture(label, &) : label
|
|
42
|
+
|
|
43
|
+
link_to_unless(disabled || current || in_link, content, href, **turbo!(opts)) do
|
|
44
|
+
@template.tag.span(content, class: "disabled", disabled: true, title: disabled.presence)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def show(*, **, &) = link(:show, @object, *, data: { turbo_stream: nil }, **, &)
|
|
49
|
+
def edit(...) = link(:edit, @object, ...)
|
|
50
|
+
def destroy(*, **, &) = link(:destroy, @object, *, method: :delete, confirm: confirm(:destroy), **, &)
|
|
51
|
+
def index(*, **, &) = link(:index, model, *, hide: true, data: { turbo_stream: nil }, **, &)
|
|
52
|
+
def new(*, **, &) = link(:new, model, *, hide: true, **, &)
|
|
53
|
+
|
|
54
|
+
def code(path = nil)
|
|
55
|
+
return unless Rails.env.development?
|
|
56
|
+
return unless @template.request.local?
|
|
57
|
+
path ||= caller_locations(1, 1).first.path
|
|
58
|
+
|
|
59
|
+
link_to "✎", "rubymine://open?file=#{path}" # &line=%{line}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def html_safe? = true
|
|
63
|
+
def to_s = p.to_s
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module CafeCar::Model
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
include CafeCar::Queryable
|
|
5
|
+
include CafeCar::Informable
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def sorted(*args)
|
|
9
|
+
return all if args.compact_blank!.empty?
|
|
10
|
+
args = args.flat_map { normalize_sort_key(_1) }
|
|
11
|
+
reorder(*args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def normalize_sort_key(key)
|
|
15
|
+
case key
|
|
16
|
+
when /,/
|
|
17
|
+
key.split(",").map { normalize_sort_key _1 }
|
|
18
|
+
when /^-(.+)$/ then { $1 => :desc }
|
|
19
|
+
else key
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class ModelInfo
|
|
3
|
+
include Caching
|
|
4
|
+
|
|
5
|
+
attr_reader :model
|
|
6
|
+
|
|
7
|
+
def self.find(object)
|
|
8
|
+
model = object.is_a?(Class) ? object : object.class
|
|
9
|
+
@cache ||= {}
|
|
10
|
+
@cache[model] ||= new(model:)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(model:)
|
|
14
|
+
@model = model
|
|
15
|
+
@field = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def columns = model.column_names
|
|
19
|
+
def field_names = model.column_names | model.reflect_on_all_attachments.map(&:name)
|
|
20
|
+
def field(method) = @field[method] ||= FieldInfo.new(model:, method:)
|
|
21
|
+
|
|
22
|
+
derive :fields, -> { Fields.new(field_names.map { field _1 }) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
module NamePatch
|
|
3
|
+
def self.patch!
|
|
4
|
+
ActiveModel::Name.class_eval do
|
|
5
|
+
def human(options = {})
|
|
6
|
+
return @human if i18n_keys.empty? || i18n_scope.empty?
|
|
7
|
+
|
|
8
|
+
key, *defaults = i18n_keys
|
|
9
|
+
defaults << options[:default] if options[:default]
|
|
10
|
+
defaults << @human
|
|
11
|
+
|
|
12
|
+
I18n.translate(key, scope: i18n_scope, count: 1, **options, default: defaults)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class Navigation
|
|
3
|
+
class Route
|
|
4
|
+
delegate :tag, :ui, :ui_class, :capture, :concat, :t, to: :@template
|
|
5
|
+
delegate :name, :requirements, to: :@route
|
|
6
|
+
|
|
7
|
+
def initialize(route, template:)
|
|
8
|
+
@route = route
|
|
9
|
+
@template = template
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def controller = requirements[:controller]
|
|
13
|
+
def action = requirements[:action]
|
|
14
|
+
def index? = action == "index"
|
|
15
|
+
def rails? = name =~ /rails/
|
|
16
|
+
def group = controller.split(?/)[..-2]
|
|
17
|
+
def text = controller.split(?/).last
|
|
18
|
+
def params = requirements.clone.tap do |p|
|
|
19
|
+
p[:controller] = "/" + p[:controller]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def icon_name = t(text, scope: "navigation.icon", default: nil)&.to_sym
|
|
23
|
+
def icon = @template.icon(icon_name, :before)
|
|
24
|
+
|
|
25
|
+
def content
|
|
26
|
+
capture do
|
|
27
|
+
concat icon
|
|
28
|
+
concat text.titleize
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def link(**opts)
|
|
33
|
+
ui.Navigation().Link(href: @template.href_for([ params ]), **opts) { content }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(template, **options)
|
|
38
|
+
@template = template
|
|
39
|
+
@options = options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
delegate :ui_class, to: :@template
|
|
43
|
+
|
|
44
|
+
def router = Rails.application.routes.router
|
|
45
|
+
def named_routes = Rails.application.routes.named_routes.to_h.values.map { Route.new(_1, template: @template) }
|
|
46
|
+
def index_routes = named_routes.select(&:index?)
|
|
47
|
+
def groups = routes.group_by(&:group)
|
|
48
|
+
|
|
49
|
+
def routes
|
|
50
|
+
@routes ||= index_routes.reject(&:rails?)
|
|
51
|
+
.uniq(&:requirements)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def recognize(obj, **)
|
|
55
|
+
req = case obj
|
|
56
|
+
when String
|
|
57
|
+
path = ActionDispatch::Journey::Router::Utils.normalize_path(path)
|
|
58
|
+
env = Rack::MockRequest.env_for(path, method: :get, **)
|
|
59
|
+
ActionDispatch::Request.new(env)
|
|
60
|
+
when ActionDispatch::Request then obj
|
|
61
|
+
else raise "cannot recognize this obj"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
router.recognize(req) do |route, params|
|
|
65
|
+
return Route.new(route, template: @template)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def current = recognize(@template.request)
|
|
70
|
+
|
|
71
|
+
def link_to(*args, **opts, &block)
|
|
72
|
+
block ||= -> { @template.tag.span(_1, class: ui_class([ :navigation, :link ], :current)) }
|
|
73
|
+
@template.link_to_unless_current(*args, class: ui_class([ :navigation, :link ]), **opts, &block)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
module OptionHelpers
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
class_attribute :option_defaults, default: {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def get_options = @options
|
|
10
|
+
|
|
11
|
+
def assign_options!
|
|
12
|
+
raise "@options is not a hash" unless get_options.is_a? Hash
|
|
13
|
+
option_defaults.each { assign_option!(_1, _2) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def assign_option!(k, default)
|
|
17
|
+
value = get_options.delete(k) { default.respond_to?(:call) ? default.call : default.clone }
|
|
18
|
+
instance_variable_set("@#{k}", value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class_methods do
|
|
22
|
+
def inherited(subclass)
|
|
23
|
+
super
|
|
24
|
+
subclass.option_defaults = option_defaults.deep_dup
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def option(*names, default: nil, accessor: true, reader: accessor, writer: accessor, presence: accessor, macro: accessor)
|
|
28
|
+
names.each do |name|
|
|
29
|
+
if respond_to?(name) and reader
|
|
30
|
+
raise ArgumentError, "Option name #{name} conflicts with existing method"
|
|
31
|
+
end
|
|
32
|
+
attr_reader name if reader
|
|
33
|
+
attr_writer name if writer
|
|
34
|
+
option_defaults[name] = default
|
|
35
|
+
define_method("#{name}?") { instance_variable_get("@#{name}").present? } if presence
|
|
36
|
+
define_singleton_method(name) { |v| option_defaults[name] = v } if macro
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def flag(*names, setter: true, **)
|
|
41
|
+
names.each do |name|
|
|
42
|
+
option(name, default: false, reader: false, **)
|
|
43
|
+
define_method(name) { instance_variable_set("@#{name}", true); self } if setter
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def option_accessor(name)
|
|
48
|
+
define_method(name) { get_options[name] }
|
|
49
|
+
define_method("#{name}=") { get_options[name] = _1 }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class CafeCar::ParamParser
|
|
2
|
+
def initialize(params)
|
|
3
|
+
@params = params
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def parsed
|
|
7
|
+
@parsed ||= @params.compact_blank
|
|
8
|
+
.then { params _1 }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def params(params)
|
|
12
|
+
params.map { |k, v| k.split(".").reverse.reduce(value(v)) { { _2 => _1 } } }
|
|
13
|
+
.reduce({}) { _1.deep_merge(_2, &method(:merge)) }
|
|
14
|
+
.with_indifferent_access
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def merge(_, a, b)
|
|
18
|
+
if a.is_a?(Array) || b.is_a?(Array)
|
|
19
|
+
[ *Array.wrap(a), *Array.wrap(b) ]
|
|
20
|
+
else
|
|
21
|
+
b
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def value(v)
|
|
26
|
+
case v
|
|
27
|
+
when Array then v.map { value(_1) }
|
|
28
|
+
when Hash then params(v).tap { _1.merge!(_1.delete("")) if _1[""] }
|
|
29
|
+
when '""', "''" then ""
|
|
30
|
+
when "nil", "" then nil
|
|
31
|
+
when /[{}\[\]]/ then value(JSON.parse(v))
|
|
32
|
+
when /,/ then value(v.split(","))
|
|
33
|
+
when /^(.*?)\.\.(\.?)(.*)$/
|
|
34
|
+
begin
|
|
35
|
+
Range.new(value($1), value($3), $2.present?)
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
v
|
|
38
|
+
end
|
|
39
|
+
when /^\$(\w+)\.(\w+)$/
|
|
40
|
+
# TODO: make less scary
|
|
41
|
+
$1.constantize.arel_table[$2]
|
|
42
|
+
else v
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
module Pluralization
|
|
3
|
+
def pluralize(locale, entry, count)
|
|
4
|
+
return super unless entry.is_a?(String) && count
|
|
5
|
+
count = 1 if count == :one
|
|
6
|
+
count = 2 unless count.is_a?(Integer)
|
|
7
|
+
entry.pluralize(count)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def localize(locale, object, format = :default, options = {})
|
|
11
|
+
options[:ordinal] ||= object.day.ordinalize
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module CafeCar::Policy
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
def policy(object) = Pundit.policy(user, object)
|
|
5
|
+
|
|
6
|
+
def model
|
|
7
|
+
@model ||= object.try(:klass) or object.is_a?(Class) ? object : object.class
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def info(method)
|
|
11
|
+
@info ||= {}
|
|
12
|
+
@info[method] ||= CafeCar[:FieldInfo].new(model:, method:)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def title_attribute
|
|
16
|
+
@title_attribute ||= displayable_attributes.first
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def logo_attribute
|
|
20
|
+
model.info.fields.listable.attachments.first&.method
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def listable_attributes
|
|
24
|
+
model.info.fields.listable.map(&:method)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def displayable_attributes
|
|
28
|
+
permitted_attribute_keys
|
|
29
|
+
.union(model.columns.map(&:name).map(&:to_sym))
|
|
30
|
+
.map { association_for_attribute(_1) || _1 }
|
|
31
|
+
.reject { filtered_attribute? _1 } - %i[id]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def permitted_fields
|
|
35
|
+
@permitted_fields ||= permitted_attribute_keys.map { info _1 }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def editable_attributes
|
|
39
|
+
permitted_fields.map(&:input_key) - permitted_fields.flat_map(&:abrogated_keys)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def displayable_associations
|
|
43
|
+
model.reflections.values
|
|
44
|
+
.select { |a| !a.options[:autosave] && !a.options[:polymorphic] }
|
|
45
|
+
.reject { _1.class_name =~ /^ActiveStorage::/ }
|
|
46
|
+
.map { _1.name.to_sym }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def permitted_attribute_keys
|
|
50
|
+
permitted_attributes.flat_map { |a| a.try(:keys) || a }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def permitted_association?(name)
|
|
54
|
+
ref = model.reflect_on_association(name)
|
|
55
|
+
|
|
56
|
+
return false if ref.has_one?
|
|
57
|
+
return permitted_attribute?(ref.foreign_key) if ref.belongs_to?
|
|
58
|
+
|
|
59
|
+
permitted_attribute?("#{ref.name.to_s.singularize}_ids")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def filtered_attribute?(attribute)
|
|
63
|
+
model.inspection_filter.filter_param(attribute, nil).present?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def permitted_attribute?(attribute)
|
|
67
|
+
permitted_attribute_keys.include?(attribute.to_sym)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def displayable_attribute?(attribute)
|
|
71
|
+
displayable_attributes.include?(attribute.to_sym)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def association_for_attribute(attribute)
|
|
75
|
+
info(attribute).reflection&.name
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
module ProcHelpers
|
|
3
|
+
def call_procs!(options, ...)
|
|
4
|
+
options.each do |k, v|
|
|
5
|
+
options[k] = v.call(...) if v.respond_to? :call
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def clone_or_call!(value, ...)
|
|
10
|
+
value.respond_to?(:call) ? value.call(...) : value.clone
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class QueryBuilder
|
|
3
|
+
require "activerecord_where_assoc"
|
|
4
|
+
|
|
5
|
+
Op = Struct.new(:op, :rhs) do
|
|
6
|
+
def initialize(op, rhs)
|
|
7
|
+
super(op.to_sym, rhs)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def flop = op.to_s.tr("<>", "><").to_sym
|
|
11
|
+
def map = Op.new(op, yield(rhs))
|
|
12
|
+
|
|
13
|
+
def arel(node) = node.public_send(arel_op, rhs)
|
|
14
|
+
|
|
15
|
+
def arel_op
|
|
16
|
+
case op
|
|
17
|
+
when :< then :lt
|
|
18
|
+
when :> then :gt
|
|
19
|
+
when :>= then :gteq
|
|
20
|
+
when :<= then :lteq
|
|
21
|
+
when :== then :eq
|
|
22
|
+
else op
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :scope
|
|
28
|
+
|
|
29
|
+
def initialize(scope)
|
|
30
|
+
@scope = scope
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def unscoped = QueryBuilder.new(@scope.unscope(:where))
|
|
34
|
+
def arel(key) = @scope.arel_table[chomp(key)]
|
|
35
|
+
def chomp(key) = key.to_s.sub(/\W+$/, "")
|
|
36
|
+
|
|
37
|
+
def parse_time(value)
|
|
38
|
+
Chronic.parse(value, guess: false, context: :past)
|
|
39
|
+
rescue NoMethodError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_range(key, value)
|
|
44
|
+
Range.new(parse_value(key, value.begin).then { _1.try(:begin) or _1 },
|
|
45
|
+
parse_value(key, value.end).then { value.exclude_end? ? _1.try(:begin) : _1.try(:end) or _1 },
|
|
46
|
+
value.exclude_end?)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse(key, value)
|
|
50
|
+
new_value = parse_value(key, value)
|
|
51
|
+
if new_value != value
|
|
52
|
+
parse(key, new_value)
|
|
53
|
+
else
|
|
54
|
+
new_value
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse_value(key, value)
|
|
59
|
+
case value
|
|
60
|
+
in Op(rhs: /^=(.*)$/)
|
|
61
|
+
Op.new("#{value.op}=", $1)
|
|
62
|
+
in Op(op: (:< | :>=), rhs: Range)
|
|
63
|
+
value.map(&:begin)
|
|
64
|
+
in Op(op: (:> | :<=), rhs: Range)
|
|
65
|
+
value.map(&:end)
|
|
66
|
+
in Range
|
|
67
|
+
parse_range(key, value)
|
|
68
|
+
in Array | Op
|
|
69
|
+
value.map { parse_value(key, _1) }
|
|
70
|
+
in "true" then true
|
|
71
|
+
in "false" then false
|
|
72
|
+
in String
|
|
73
|
+
case column(key)&.type || reflection(key)&.macro
|
|
74
|
+
when :datetime then parse_time(value) || value
|
|
75
|
+
when :integer then value.to_i
|
|
76
|
+
when :float then value.to_f
|
|
77
|
+
when :belongs_to, :has_many, :has_one
|
|
78
|
+
value.to_i
|
|
79
|
+
else value
|
|
80
|
+
end
|
|
81
|
+
else value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def update!(&)
|
|
86
|
+
scope = yield @scope
|
|
87
|
+
@scope = scope if scope
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def not!(&)
|
|
92
|
+
inverted = unscoped.tap { _1.instance_exec(&) }.scope.invert_where
|
|
93
|
+
update! { _1.and(inverted) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def column(name) = @scope.columns_hash[name.to_s]
|
|
97
|
+
def reflection(name) = @scope.reflect_on_association(name)
|
|
98
|
+
def association?(name) = reflection(name).present?
|
|
99
|
+
def attribute?(name) = column(name).present?
|
|
100
|
+
def scope?(name) = name.intern.in? @scope.local_methods
|
|
101
|
+
|
|
102
|
+
def arel!(node) = @scope.where!(node)
|
|
103
|
+
|
|
104
|
+
def param!(key, value)
|
|
105
|
+
case key
|
|
106
|
+
when /^(.*?)\s*!$/
|
|
107
|
+
not! { param!($1, value) }
|
|
108
|
+
when /^(.*?)\s*~$/
|
|
109
|
+
param!($1, Regexp.new(value, Regexp::IGNORECASE))
|
|
110
|
+
when /^(.*?)\s*([<>]=?)$/
|
|
111
|
+
param!($1, Op.new($2, value))
|
|
112
|
+
when method(:association?)
|
|
113
|
+
association!(key, value)
|
|
114
|
+
when method(:attribute?)
|
|
115
|
+
attribute!(key, value)
|
|
116
|
+
when method(:scope?)
|
|
117
|
+
scope!(key, value)
|
|
118
|
+
else
|
|
119
|
+
raise MissingAttributeError, "can't find #{key.inspect} on #{@scope.model_name}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def attribute!(key, value)
|
|
124
|
+
case [ key, value ]
|
|
125
|
+
in _, Regexp
|
|
126
|
+
@scope.where!(arel(key).matches_regexp(value.source, !value.casefold?))
|
|
127
|
+
in _, Op
|
|
128
|
+
@scope.where!(parse(key, value).arel(arel(key)))
|
|
129
|
+
else @scope.where!(key => parse(key, value))
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def association!(name, value, ...)
|
|
134
|
+
update! do
|
|
135
|
+
case value
|
|
136
|
+
when true then @scope.where_assoc_exists(name)
|
|
137
|
+
when false then @scope.where_assoc_not_exists(name)
|
|
138
|
+
when Integer, Range, /^\d+$/
|
|
139
|
+
@scope.where_assoc_count(parse(name, value), :==, name)
|
|
140
|
+
when Op
|
|
141
|
+
value = parse(name, value)
|
|
142
|
+
@scope.where_assoc_count(value.rhs, value.flop, name)
|
|
143
|
+
else @scope.where_assoc_exists(name) { all.query!(value, ...) }
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def scope!(name, value)
|
|
149
|
+
value = parse_value(name, value)
|
|
150
|
+
arity = (@scope.scopes[name] || @scope.method(name)).arity
|
|
151
|
+
value = nil if arity == 0 and value == true
|
|
152
|
+
|
|
153
|
+
update! { _1.public_send(name, *value) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def search!(term)
|
|
157
|
+
@scope.search!(term) if @scope.respond_to?(:search!)
|
|
158
|
+
@scope.query!("body~": term) if @scope < ::ActionText::RichText
|
|
159
|
+
update! { _1.search(term) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def query!(params = nil)
|
|
163
|
+
case params
|
|
164
|
+
when Hash then params.each { param!(_1, _2) }
|
|
165
|
+
when Array then params.each { query! _1 }
|
|
166
|
+
when String then search!(params)
|
|
167
|
+
# when Arel::Nodes::Node then arel!(params)
|
|
168
|
+
when nil
|
|
169
|
+
else raise ArgumentError, "cannot query on #{params}"
|
|
170
|
+
end
|
|
171
|
+
self
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def query(...) = clone.update!(&:all).query!(...)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Article.query do
|
|
179
|
+
# published
|
|
180
|
+
# user { name(/bob/i) }
|
|
181
|
+
# user.name(/bob/i)
|
|
182
|
+
# end
|
|
183
|
+
#
|
|
184
|
+
# Article.query(published: true, user: {name: /bob/i})
|
|
185
|
+
#
|
|
186
|
+
# Article.published(true).where_assoc_exists(:user) { where(name: /bob/i) }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module CafeCar::Queryable
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
class_methods do
|
|
5
|
+
def scope(name, body)
|
|
6
|
+
scopes[name] = body
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def sample = offset(rand(count)).first
|
|
11
|
+
|
|
12
|
+
def query(params) = query_builder.query(params).scope
|
|
13
|
+
def query!(params) = query_builder.query!(params).scope
|
|
14
|
+
|
|
15
|
+
def query_builder
|
|
16
|
+
CafeCar::QueryBuilder.new(all)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def scopes
|
|
20
|
+
@scopes ||= {}.with_indifferent_access
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def local_methods
|
|
24
|
+
@local_methods ||= public_methods -
|
|
25
|
+
ActiveRecord::Base.public_methods -
|
|
26
|
+
Kaminari::ConfigurationMethods::ClassMethods.instance_methods
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|