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,155 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class Component
|
|
3
|
+
concerning :Macros do
|
|
4
|
+
included do
|
|
5
|
+
class_attribute :option_defaults, default: {}
|
|
6
|
+
class_attribute :attribute_defaults, default: {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def extract_options!(options)
|
|
10
|
+
@options = options.extract!(*option_defaults.keys).with_defaults!(option_defaults)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def inherited(subclass)
|
|
15
|
+
super
|
|
16
|
+
subclass.option_defaults = option_defaults.deep_dup
|
|
17
|
+
subclass.attribute_defaults = attribute_defaults.deep_dup
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def component(*names, **, &)
|
|
21
|
+
names.each do |name|
|
|
22
|
+
define_class(name, CafeCar[:Component], **, &)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def flag(flag)
|
|
27
|
+
include Module.new do
|
|
28
|
+
define_method(flag) { |v| @options[flag] = v.nil? ? true : v }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def option(name, default: nil, accessor: true, reader: accessor, writer: accessor, presence: accessor, macro: accessor)
|
|
33
|
+
option_defaults[name] = default
|
|
34
|
+
define_singleton_method(name) { |v| option_defaults[name] = v } if macro
|
|
35
|
+
include Module.new {
|
|
36
|
+
define_method(name) { options[name] } if reader
|
|
37
|
+
define_method("#{name}=") { |v| options[name] = v } if writer
|
|
38
|
+
define_method("#{name}?") { options[name].present? } if presence
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.method_missing(name, v)
|
|
45
|
+
attribute_defaults[name] = v
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
attr_reader :flags, :options, :attributes
|
|
49
|
+
|
|
50
|
+
delegate :render, :capture, :safe_join, :ui_class, :context?, to: :@template
|
|
51
|
+
|
|
52
|
+
option :tag, default: :div
|
|
53
|
+
option :class, accessor: false
|
|
54
|
+
option :data, default: {}
|
|
55
|
+
option :href
|
|
56
|
+
option :tip
|
|
57
|
+
|
|
58
|
+
def initialize(template, name, *args, **attributes, &block)
|
|
59
|
+
@template = template
|
|
60
|
+
@names = [ *name ].map(&:underscore)
|
|
61
|
+
@flags = args.extract! { _1.is_a? Symbol }
|
|
62
|
+
@args = args.flatten.compact_blank
|
|
63
|
+
@attributes = attributes.with_defaults!(attribute_defaults)
|
|
64
|
+
@children = attributes.extract_if! { _1 =~ /^[A-Z]\w*$/ }
|
|
65
|
+
@block = block
|
|
66
|
+
extract_options!(@attributes)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def name = @names.last
|
|
70
|
+
def tag = href? ? :a : super
|
|
71
|
+
def href? = super && !context?(:a) && !current_href?
|
|
72
|
+
|
|
73
|
+
def href
|
|
74
|
+
@template.href_for(super) if href?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def current_href? = options[:href]&.then { @template.current_href?(_1, check_parameters: true) }
|
|
78
|
+
def ancestor_href? = options[:href]&.then { @template.ancestor_href?(_1) }
|
|
79
|
+
|
|
80
|
+
def data
|
|
81
|
+
super.merge({ tip: }.compact_blank)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def partial? = @template.partial?(partial_name)
|
|
85
|
+
def partial_name = "ui/" + @names.join("_")
|
|
86
|
+
|
|
87
|
+
def render_partial
|
|
88
|
+
render(partial_name, options:, flags:, name => self, **options) { content }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def selector = class_names.join(?_).then { ?. + _1 }
|
|
92
|
+
|
|
93
|
+
def class_names = @names.map(&:camelize)
|
|
94
|
+
|
|
95
|
+
def class_name
|
|
96
|
+
@template.ui_class(class_names, *flags, *options[:class], options[:tag].to_s => href?,
|
|
97
|
+
current: current_href?,
|
|
98
|
+
ancestor: ancestor_href? && !current_href?)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def attributes
|
|
102
|
+
@attributes.merge(class: class_name, href:, data:)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def children
|
|
106
|
+
@children.map { |name, content| send(name) { content } }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def blank?
|
|
110
|
+
@block and body and content.blank? || !content.match?(/^.*?[^<\s]/) || content.gsub(/<!--.*?-->/, "").blank?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def body
|
|
114
|
+
@body ||= context { partial? ? render_partial : content }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def content
|
|
118
|
+
@content ||= safe_join [ *children, *@args, *(capture(self, &@block) if @block) ]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def context(&)
|
|
122
|
+
href? ? @template.context(:a, &) : capture(&)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def wrapper(&)
|
|
126
|
+
@template.content_tag(tag, **attributes, &)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def ~@ = @template.concat(self)
|
|
130
|
+
def +(o) = safe_join([ self, o ])
|
|
131
|
+
def <<(o) = @template.concat(o)
|
|
132
|
+
|
|
133
|
+
def html_safe? = true
|
|
134
|
+
|
|
135
|
+
def to_s = to_html
|
|
136
|
+
|
|
137
|
+
def to_html
|
|
138
|
+
return "" if blank?
|
|
139
|
+
wrapper { body }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def child(name, ...)
|
|
143
|
+
c = self.class.try { const_defined?(name) ? const_get(name) : Component }
|
|
144
|
+
c.new(@template, [ *@names, name ], ...)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def method_missing(name, ...)
|
|
148
|
+
if name =~ /^[A-Z]/
|
|
149
|
+
child(name, ...)
|
|
150
|
+
else
|
|
151
|
+
super
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class Context
|
|
3
|
+
def initialize(template, prefix: nil)
|
|
4
|
+
@template = template
|
|
5
|
+
@prefix = prefix
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def class(name, ...) = @template.ui_class([ *@prefix, *name ], ...)
|
|
9
|
+
# def wrapper(...) = Component.new(@template, [*@prefix], ...).wrapper(...)
|
|
10
|
+
def <<(obj) = @template.concat(obj)
|
|
11
|
+
|
|
12
|
+
def method_missing(name, ...)
|
|
13
|
+
c = CafeCar[:UI].const_try(name) || Component
|
|
14
|
+
c.new(@template, [ *@prefix, name ], ...)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module CafeCar::Controller::Filtering
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
included do
|
|
5
|
+
helper_method :parsed_params, :dot_params, :filtered?
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def filtered(scope)
|
|
11
|
+
scope.query(parsed_params[""])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def filtered?
|
|
15
|
+
parsed_params[""].present?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def dot_params
|
|
19
|
+
request.params.slice(*request.params.keys.grep(/^\./))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parsed_params
|
|
23
|
+
@parsed_params ||=
|
|
24
|
+
if request.get? || request.head?
|
|
25
|
+
CafeCar::ParamParser.new(request.params).parsed
|
|
26
|
+
else
|
|
27
|
+
request.params
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require "responders"
|
|
2
|
+
|
|
3
|
+
module CafeCar
|
|
4
|
+
module Controller
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
include Pundit::Authorization
|
|
8
|
+
include Filtering, Authentication
|
|
9
|
+
|
|
10
|
+
class_methods do
|
|
11
|
+
def model(model)
|
|
12
|
+
define_method(:model) { @model ||= model }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def default_view(v = @default_view || "table")
|
|
16
|
+
@default_view = v.to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cafe_car(only: nil, except: nil, model: nil)
|
|
20
|
+
_only = ->(actions) do
|
|
21
|
+
actions -= except if except
|
|
22
|
+
actions &= only if only
|
|
23
|
+
actions
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
self.model model if model
|
|
27
|
+
|
|
28
|
+
rescue_from ::ActiveRecord::RecordInvalid,
|
|
29
|
+
::ActiveModel::ValidationError, with: :render_invalid_object
|
|
30
|
+
|
|
31
|
+
rescue_from ::Pundit::NotAuthorizedError, with: :render_unauthorized
|
|
32
|
+
|
|
33
|
+
append_cafe_car_views
|
|
34
|
+
|
|
35
|
+
respond_to :json, :html, :turbo_stream
|
|
36
|
+
|
|
37
|
+
before_action :set_current_attributes
|
|
38
|
+
|
|
39
|
+
before_action :find_object, only: _only.(%i[show edit update destroy])
|
|
40
|
+
before_action :build_object, only: _only.(%i[new create])
|
|
41
|
+
before_action :find_objects, only: _only.(%i[index])
|
|
42
|
+
before_action :assign_attributes, only: _only.(%i[create update])
|
|
43
|
+
before_action :authorize!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def append_cafe_car_views
|
|
47
|
+
append_view_path CafeCar::Engine.root.join("app/views/cafe_car")
|
|
48
|
+
append_view_path "app/views/cafe_car"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def define_callbacks_with_helpers(*names, **)
|
|
52
|
+
define_callbacks(*names, **)
|
|
53
|
+
|
|
54
|
+
names.each do |name|
|
|
55
|
+
%i[before around after].each do |kind|
|
|
56
|
+
define_singleton_method "#{kind}_#{name}" do |*args, **opts|
|
|
57
|
+
set_callback(name, kind, *args, prepend: true, **opts)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
define_singleton_method "skip_#{kind}_#{name}" do |*args|
|
|
61
|
+
skip_callback(name, kind, *args)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
included do
|
|
69
|
+
self.responder = CafeCar[:ApplicationResponder]
|
|
70
|
+
default_form_builder CafeCar[:FormBuilder]
|
|
71
|
+
|
|
72
|
+
define_callbacks_with_helpers :render, :update, :create, :destroy
|
|
73
|
+
add_flash_types :success, :warning, :error
|
|
74
|
+
|
|
75
|
+
helper_method :model, :model_name, :object, :objects
|
|
76
|
+
helper_method :action, :scope, :view, :default_view
|
|
77
|
+
|
|
78
|
+
helper Helpers
|
|
79
|
+
delegate :present, :href_for, :namespace, to: :helpers
|
|
80
|
+
|
|
81
|
+
after_action :verify_authorized, :verify_policy_scoped
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def index = respond_with objects
|
|
85
|
+
def show = respond_with object
|
|
86
|
+
def new = respond_with object
|
|
87
|
+
def edit = respond_with object
|
|
88
|
+
|
|
89
|
+
def create
|
|
90
|
+
run_callbacks(:create) { object.save! }
|
|
91
|
+
respond_with object
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def update
|
|
95
|
+
run_callbacks(:update) { object.save! }
|
|
96
|
+
respond_with object
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def destroy
|
|
100
|
+
run_callbacks(:destroy) { object.destroy! }
|
|
101
|
+
respond_with object
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def respond_with(*resources, **options, &block)
|
|
105
|
+
super(*namespace, *resources, **options, &block)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def authorize!
|
|
111
|
+
return authorize object if object
|
|
112
|
+
return authorize objects if objects
|
|
113
|
+
raise "nothing to authorize! Define self.object or self.objects"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def flash!
|
|
117
|
+
flash[:success] = present(object).i18n("#{action_name}_html", scope: :flashes)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def sorted(scope)
|
|
121
|
+
scope.sorted(*params[:sort].presence)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def paginated(scope, page: params[:page], per: params[:per])
|
|
125
|
+
scope.page(page).per(per)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_object
|
|
129
|
+
self.object = scope.new
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def find_object
|
|
133
|
+
delimiter = model.param_delimiter
|
|
134
|
+
id = unwrap params.extract_value(:id, delimiter:)
|
|
135
|
+
self.object = scope.except(:limit, :offset).find(id)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def unwrap(arr) = arr.size == 1 ? arr.first : arr
|
|
139
|
+
|
|
140
|
+
def find_objects
|
|
141
|
+
self.objects = scope
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def assign_attributes
|
|
145
|
+
object.assign_attributes(permitted_attributes(object))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def scope = model.all.then { policy_scope _1 }
|
|
149
|
+
.then { sorted _1 }
|
|
150
|
+
.then { filtered _1 }
|
|
151
|
+
.then { paginated _1 }
|
|
152
|
+
def objects = instance_variable_get("@#{model_name.plural}")
|
|
153
|
+
def objects=(value)
|
|
154
|
+
instance_variable_set("@#{model_name.plural}", value)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def object = instance_variable_get("@#{model_name.singular}")
|
|
158
|
+
def object=(value)
|
|
159
|
+
instance_variable_set("@#{model_name.singular}", value)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def model_name = model.model_name
|
|
163
|
+
|
|
164
|
+
def model
|
|
165
|
+
@model ||= self.class.name.gsub(/.*::|Controller$/, "")
|
|
166
|
+
.classify
|
|
167
|
+
.then { self.class.module_parent.const_get _1 }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def render_invalid_object
|
|
171
|
+
render(object.persisted? ? "edit" : "new", status: :unprocessable_content)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def render_unauthorized
|
|
175
|
+
if !sessions_available?
|
|
176
|
+
head :forbidden
|
|
177
|
+
elsif authenticated?
|
|
178
|
+
redirect_back_or_to root_path
|
|
179
|
+
else
|
|
180
|
+
request_authentication
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# def default_render(...) = run_callbacks(:render) { super }
|
|
185
|
+
|
|
186
|
+
def default_url_options
|
|
187
|
+
{} # { locale: I18n.locale }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def action
|
|
191
|
+
@action ||= ActiveSupport::StringInquirer.new(action_name)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def default_view = self.class.default_view
|
|
195
|
+
def view
|
|
196
|
+
params.fetch(:view) { default_view }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def _render_with_renderer_json(obj, options)
|
|
200
|
+
# permitted_attributes is record-oriented, so ask a record for the column
|
|
201
|
+
# list even when serializing a collection.
|
|
202
|
+
record = obj.is_a?(CafeCar::Model) ? obj : obj.klass.new
|
|
203
|
+
options[:only] ||= [ :id ] | policy(record).displayable_attributes
|
|
204
|
+
|
|
205
|
+
if obj.is_a?(CafeCar::Model)
|
|
206
|
+
options[:include] ||= policy(obj).displayable_associations
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
super obj, **options
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def set_current_attributes
|
|
213
|
+
CafeCar[:Current].request_id = request.uuid
|
|
214
|
+
CafeCar[:Current].user_agent = request.user_agent
|
|
215
|
+
CafeCar[:Current].ip_address = request.ip
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Array
|
|
4
|
+
def overlap(other)
|
|
5
|
+
0.upto(size) do |i|
|
|
6
|
+
if other.start_with? drop(i)
|
|
7
|
+
return slice(0, i) + other
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def extract!(pattern = nil, &block)
|
|
13
|
+
block = -> { pattern === _1 } if pattern
|
|
14
|
+
return to_enum(:extract!) { size } unless block
|
|
15
|
+
|
|
16
|
+
extracted_elements = []
|
|
17
|
+
|
|
18
|
+
reject! do |element|
|
|
19
|
+
extracted_elements << element if block.(element)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
extracted_elements
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Hash
|
|
4
|
+
# Removes and returns a hash containing the key/value pairs for which the
|
|
5
|
+
# block returns a true value given the key.
|
|
6
|
+
#
|
|
7
|
+
# hash = {:a => 1, "b" => 2, :c => 3}
|
|
8
|
+
# hash.extract_if! { _1.is_a? Symbol } #=> {a: 1, c: 3}
|
|
9
|
+
# hash #=> {"b" => 2 }
|
|
10
|
+
def extract_if!
|
|
11
|
+
each_with_object(self.class.new) do |(key, value), result|
|
|
12
|
+
result[key] = delete(key) if yield(key, value)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Module
|
|
4
|
+
# Shorthand for `const_set(name.camelize, Class.new(parent) { ... })`.
|
|
5
|
+
# Useful when defining classes in macros.
|
|
6
|
+
def define_class(name, *, **macros, &)
|
|
7
|
+
name = name.to_s.camelize
|
|
8
|
+
raise NameError, "class exists: #{name}" if const_defined?(name, false)
|
|
9
|
+
klass = Class.new(*) do
|
|
10
|
+
macros.each { |key, value| send(key, *value) }
|
|
11
|
+
class_eval(&) if block_given?
|
|
12
|
+
end
|
|
13
|
+
const_set name, klass
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require "chronic"
|
|
2
|
+
require "haml-rails"
|
|
3
|
+
require "kaminari"
|
|
4
|
+
require "image_processing"
|
|
5
|
+
require "propshaft"
|
|
6
|
+
require "pundit"
|
|
7
|
+
require "importmap-rails"
|
|
8
|
+
require "turbo-rails"
|
|
9
|
+
require "potter"
|
|
10
|
+
require "potter/type"
|
|
11
|
+
|
|
12
|
+
module CafeCar
|
|
13
|
+
class Engine < ::Rails::Engine
|
|
14
|
+
config.autoload_paths += paths["lib"].to_a
|
|
15
|
+
|
|
16
|
+
initializer "cafe_car.inflections" do
|
|
17
|
+
ActiveSupport::Inflector.inflections do |i|
|
|
18
|
+
i.acronym "UI"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
initializer "cafe_car.i18n" do |app|
|
|
23
|
+
app.reloader.to_prepare do
|
|
24
|
+
CafeCar::NamePatch.patch!
|
|
25
|
+
I18n::Backend::Simple.include(CafeCar::Pluralization)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
initializer "cafe_car.assets" do |app|
|
|
30
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
initializer "cafe_car.preload_header" do |app|
|
|
34
|
+
app.config.action_view.preload_links_header = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
initializer "cafe_car.filter_parameters" do |app|
|
|
38
|
+
app.config.filter_parameters |= %i[authorization passw secret token _key crypt salt certificate otp ssn]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
initializer "cafe_car.importmap", before: "importmap" do |app|
|
|
42
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
43
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
initializer "cafe_car.active_record" do |app|
|
|
47
|
+
app.reloader.to_prepare do
|
|
48
|
+
::ActiveRecord::Base.include(Model)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
initializer "cafe_car.routing" do |app|
|
|
53
|
+
app.reloader.to_prepare do
|
|
54
|
+
::ActionDispatch::Routing::Mapper.include(Routing)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
initializer "cafe_car.chronic" do |app|
|
|
59
|
+
app.reloader.to_prepare do
|
|
60
|
+
Chronic.time_class = Time.zone
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
initializer "cafe_car.sqlite_regexp" do |app|
|
|
65
|
+
ActiveSupport.on_load :active_record_sqlite3adapter do
|
|
66
|
+
::Arel::Visitors::SQLite.include(Visitors::SQLite)
|
|
67
|
+
include ActiveRecord::SQLite3Extension
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
initializer "cafe_car.turbo" do
|
|
72
|
+
ActiveSupport.on_load :turbo_streams_tag_builder do
|
|
73
|
+
include TurboTagBuilder
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
initializer "cafe_car.responders" do
|
|
78
|
+
require "responders"
|
|
79
|
+
config.responders.flash_keys = [ :success, :error ]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
initializer "cafe_car.field_with_errors" do
|
|
83
|
+
ActionView::Base.field_error_proc = proc { _1.html_safe }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
initializer "cafe_car.console" do |app|
|
|
87
|
+
app.console do
|
|
88
|
+
TOPLEVEL_BINDING.eval("self").instance_exec do
|
|
89
|
+
def logger = Rails.logger
|
|
90
|
+
|
|
91
|
+
if defined?(FactoryBot)
|
|
92
|
+
include FactoryBot::Syntax::Methods
|
|
93
|
+
logger.info "FactoryBot methods enabled."
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
logger.info "SQL logs enabled."
|
|
97
|
+
|
|
98
|
+
ApplicationController.allow_forgery_protection = false
|
|
99
|
+
logger.info "CSRF disabled to enable app.post calls."
|
|
100
|
+
|
|
101
|
+
def present(...) = CafeCar[:Presenter].present(helper, ...)
|
|
102
|
+
def ui(...) = helper.ui(...)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class FieldBuilder
|
|
3
|
+
attr_reader :form, :method, :as
|
|
4
|
+
|
|
5
|
+
delegate :object, :info, to: :form
|
|
6
|
+
|
|
7
|
+
def initialize(method:, form:, template:, **options, &block)
|
|
8
|
+
@method = method
|
|
9
|
+
@form = form
|
|
10
|
+
@template = template
|
|
11
|
+
@options = options
|
|
12
|
+
@block = block
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def info = form.info(@method)
|
|
16
|
+
def wrapper(...) = @template.ui.Field(...)
|
|
17
|
+
def input(...) = send_to_form(:input, ...)
|
|
18
|
+
def label(...) = send_to_form_with_text(:label, ...)
|
|
19
|
+
def hint(...) = send_to_form_with_text(:hint, ...)
|
|
20
|
+
def error(...) = send_to_form_with_text(:error, ...)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def html_safe? = true
|
|
24
|
+
def to_s
|
|
25
|
+
return input if info.polymorphic? and object.new_record?
|
|
26
|
+
partial = %W[#{info.type}_field field].find { @template.partial?(_1) }
|
|
27
|
+
@template.render(partial, field: self, **@options)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def add_class(*args, opts) = { class: @template.ui_class([ :field, *args ], *opts.delete(:class)), **opts }
|
|
33
|
+
|
|
34
|
+
def send_to_form_with_text(method, text = @options.delete(method), **, &)
|
|
35
|
+
return if text == false
|
|
36
|
+
send_to_form(method, *text, **, &)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def send_to_form(method, *, **opts, &)
|
|
40
|
+
return if @options[method] == false
|
|
41
|
+
form.public_send(method, @method, *, **add_class(*method, opts), &)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|