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,144 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class FieldInfo
|
|
3
|
+
attr_reader :method, :model
|
|
4
|
+
alias_method :name, :method
|
|
5
|
+
|
|
6
|
+
delegate :model_name, to: :@model
|
|
7
|
+
|
|
8
|
+
def initialize(model:, method:)
|
|
9
|
+
# raise "Cannot get FieldInfo for nil method" if method.nil?
|
|
10
|
+
@method = method&.to_sym
|
|
11
|
+
@model = model
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def info(method) = model.info.field(method)
|
|
15
|
+
|
|
16
|
+
def id? = method =~ /_ids?$/
|
|
17
|
+
def constant? = method.in? %i[id created_at updated_at]
|
|
18
|
+
def timestamp? = type.in? %i[date datetime time]
|
|
19
|
+
|
|
20
|
+
def association?
|
|
21
|
+
return if @method.nil?
|
|
22
|
+
model.reflect_on_association(@method).present?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def associated? = reflection.present?
|
|
26
|
+
def polymorphic? = reflection&.polymorphic?
|
|
27
|
+
def digest? = method =~ /_digest$/
|
|
28
|
+
def password? = type == :password
|
|
29
|
+
def rich_text? = reflection&.name =~ /^rich_text_(\w+)$/
|
|
30
|
+
def attachment? = model.reflect_on_attachment(method)
|
|
31
|
+
def collection = reflection.klass.all
|
|
32
|
+
def reflection
|
|
33
|
+
return if @method.nil?
|
|
34
|
+
model.try(:reflect_on_association, @method) || reflections_by_attribute[@method]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def abrogated_keys
|
|
38
|
+
[ *reflection&.foreign_type&.to_sym ]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def displayable = reflection&.name&.then { info(_1) } || self
|
|
42
|
+
|
|
43
|
+
def default_type
|
|
44
|
+
case method
|
|
45
|
+
when :controls then method
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reflection_type = reflection&.macro
|
|
50
|
+
def attribute_type = model.type_for_attribute(@method)&.type
|
|
51
|
+
def digest_type
|
|
52
|
+
key = @method.to_s.chomp("_confirmation")
|
|
53
|
+
model.type_for_attribute("#{key}_digest")&.type && :password
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def attachment_type
|
|
57
|
+
:attachment if attachment?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def nested_attributes_type
|
|
61
|
+
key = @method.to_s.chomp("_attributes").to_sym
|
|
62
|
+
model.nested_attributes_options.key?(key) && :nested
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def polymorphic_methods = [ reflection.foreign_type, reflection.foreign_key ]
|
|
66
|
+
|
|
67
|
+
def placeholder = i18n(:placeholder)
|
|
68
|
+
def autocomplete = i18n(:autocomplete)
|
|
69
|
+
def hint = i18n(:hint)
|
|
70
|
+
def label = i18n(:label, default: human)
|
|
71
|
+
def prompt = i18n(:prompt, default: "Select #{human.downcase}...")
|
|
72
|
+
def human(...) = @method&.then { model.human_attribute_name(_1, ...) }
|
|
73
|
+
def required? = validator?(:presence)
|
|
74
|
+
|
|
75
|
+
def validator?(kind, **options)
|
|
76
|
+
model.validators_on(@method).any? { _1.kind == kind and _1.options >= options }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def i18n_key = model_name.i18n_key
|
|
80
|
+
def i18n(key, **opts)
|
|
81
|
+
return if @method.nil?
|
|
82
|
+
I18n.t(@method, scope: [ :helpers, key, i18n_key ], raise: true, **opts)
|
|
83
|
+
rescue I18n::MissingTranslationData
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def type
|
|
87
|
+
return if @method.nil?
|
|
88
|
+
@type ||= reflection_type || attribute_type || digest_type || attachment_type ||
|
|
89
|
+
default_type || nested_attributes_type ||
|
|
90
|
+
raise(NoMethodError.new "Can't find attribute :#{@method} on #{model_name}", @method)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def width
|
|
94
|
+
case type
|
|
95
|
+
when :text, :string, :json
|
|
96
|
+
# "minmax(10em, fit-content)"
|
|
97
|
+
# "minmax(10em, 1fr)"
|
|
98
|
+
"minmax(10em, auto)"
|
|
99
|
+
# "minmax(min-content, auto)"
|
|
100
|
+
else "min-content"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def input
|
|
105
|
+
case type
|
|
106
|
+
when :string then :text_field
|
|
107
|
+
when :decimal then :text_field
|
|
108
|
+
when :text, :json then :text_area
|
|
109
|
+
when :integer then :number_field
|
|
110
|
+
when :date then :date_field
|
|
111
|
+
when :datetime then :datetime_field
|
|
112
|
+
when :password then :password_field
|
|
113
|
+
when :nested then :fields_for
|
|
114
|
+
when :attachment then :file_field
|
|
115
|
+
when :belongs_to, :has_many then :association
|
|
116
|
+
when :has_one
|
|
117
|
+
rich_text? ? :rich_text_area : nil
|
|
118
|
+
else raise "Missing input type for #{model_name}##{@method} of type :#{type}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def input_key
|
|
123
|
+
case type
|
|
124
|
+
when :belongs_to then reflection.foreign_key
|
|
125
|
+
else method
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
@@reflections_by_attribute = {}
|
|
130
|
+
def reflections_by_attribute
|
|
131
|
+
return {} unless model.respond_to? :reflections
|
|
132
|
+
@@reflections_by_attribute[model] ||=
|
|
133
|
+
model.reflections.values.index_by do |r|
|
|
134
|
+
case [ r.macro, r.name ]
|
|
135
|
+
in [ :belongs_to, * ] then r.foreign_key
|
|
136
|
+
in [ :has_many, * ] then "#{r.name.to_s.singularize}_ids"
|
|
137
|
+
in [ :has_one, /^rich_text_(\w+)$/ ] then $1
|
|
138
|
+
in [ :has_one, * ] then r.name
|
|
139
|
+
else raise NoMethodError.new("Not yet implemented :#{r.macro}")
|
|
140
|
+
end
|
|
141
|
+
end.with_indifferent_access
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class Fields < Array
|
|
3
|
+
include Caching
|
|
4
|
+
|
|
5
|
+
derive :editable, -> { Fields.new reject(&:constant?) }
|
|
6
|
+
derive :listable, -> { Fields.new editable.reject(&:digest?) }
|
|
7
|
+
derive :attachments, -> { Fields.new select(&:attachment?) }
|
|
8
|
+
derive :timestamps, -> { Fields.new select(&:timestamp?) }
|
|
9
|
+
|
|
10
|
+
derive :by_name, -> { index_by(&:name).with_indifferent_access }
|
|
11
|
+
derive :names, -> { map(&:name) }
|
|
12
|
+
|
|
13
|
+
def reverse = Fields.new(super)
|
|
14
|
+
|
|
15
|
+
def sort_with(obj)
|
|
16
|
+
Fields.new(sort_by { obj.try(_1.name) })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def has?(name) = by_name.key?(name)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CafeCar::Filter::FieldInfo < CafeCar[:FieldInfo]
|
|
2
|
+
def i18n(key, **opts)
|
|
3
|
+
I18n.t(@method, scope: [ :helpers, :filter, key, i18n_key ], raise: true, **opts)
|
|
4
|
+
rescue I18n::MissingTranslationData
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def input
|
|
8
|
+
case type
|
|
9
|
+
when :string then :text_field
|
|
10
|
+
when :text then :text_field
|
|
11
|
+
when :decimal then :text_field # :range_field
|
|
12
|
+
when :integer then :text_field # :range_field
|
|
13
|
+
when :date then :text_field
|
|
14
|
+
when :datetime then :text_field
|
|
15
|
+
when :password then :password_field
|
|
16
|
+
when :belongs_to, :has_many then :association
|
|
17
|
+
when :has_one
|
|
18
|
+
rich_text? ? :text_field : nil
|
|
19
|
+
else raise "Missing input type for #{model_name}##{@method} of type :#{type}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module CafeCar::Filter
|
|
2
|
+
class FormBuilder < CafeCar[:FormBuilder]
|
|
3
|
+
def self.dotted_name(method)
|
|
4
|
+
define_method method do |key, *args, **opts, &block|
|
|
5
|
+
super(key, *args, name: field_name(key), **opts, &block)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
instance_methods.grep(/_(field|select)$/).each do |method|
|
|
10
|
+
dotted_name method
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def clean(method) = method.to_s.sub(/^\W+|\W+$/, "")
|
|
14
|
+
def info(method) = super(clean(method))
|
|
15
|
+
|
|
16
|
+
def field_name(*methods, multiple: false, index: @options[:index])
|
|
17
|
+
# TODO: handle multiple/index
|
|
18
|
+
[ "", *methods ].join(".")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class FilterBuilder
|
|
3
|
+
delegate :model_name, :klass, to: :@objects
|
|
4
|
+
|
|
5
|
+
def initialize(objects, params)
|
|
6
|
+
@objects = objects
|
|
7
|
+
@params = params
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def model = @objects.klass
|
|
11
|
+
def to_key = [ model_name.param_key, :filters ]
|
|
12
|
+
def to_model = self
|
|
13
|
+
def persisted? = false
|
|
14
|
+
def errors = Hash.new([])
|
|
15
|
+
|
|
16
|
+
def method_missing(name, *, &)
|
|
17
|
+
@params.dig("", name)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
|
3
|
+
include Resolver
|
|
4
|
+
|
|
5
|
+
delegate :ui, to: :@template
|
|
6
|
+
|
|
7
|
+
def initialize(...)
|
|
8
|
+
super
|
|
9
|
+
@fields = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def model = @object.is_a?(Class) ? @object : @object.class
|
|
13
|
+
def policy = @template.policy(@object)
|
|
14
|
+
def show(...) = ui.Input { @template.present(@object).show(...) }
|
|
15
|
+
|
|
16
|
+
def association(method, collection: nil, **options)
|
|
17
|
+
info = info(method)
|
|
18
|
+
|
|
19
|
+
return show(info.input_key) if info.polymorphic? and object.persisted?
|
|
20
|
+
return hidden(*info.polymorphic_methods) if info.polymorphic?
|
|
21
|
+
|
|
22
|
+
collection ||= info.collection
|
|
23
|
+
# options[:prompt] ||= info.prompt
|
|
24
|
+
options[:include_blank] ||= info.prompt
|
|
25
|
+
|
|
26
|
+
input(info.input_key, collection, :id, -> { @template.present(_1).title }, as: :collection_select, **options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def hidden(*methods, **, &)
|
|
30
|
+
methods.map { input(_1, as: :hidden_field, **, &) }
|
|
31
|
+
.then { @template.safe_join(_1) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def field(method, **, &)
|
|
35
|
+
@fields[method] ||= const(:FieldBuilder).new(method:, form: self, template: @template, **, &)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def label(method, text = info(method).label, required: info(method).required?, **, &)
|
|
39
|
+
super(method, @template.safe_join([ text, required ? "*" : "" ]), required:, **, &)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def submit(value = nil, **options)
|
|
43
|
+
options[:class] ||= ui.class(:button, :primary)
|
|
44
|
+
super(value, options)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def info(method) = model.info.field(method)
|
|
48
|
+
|
|
49
|
+
def input(method, *args, as: nil, **options)
|
|
50
|
+
info = info(method)
|
|
51
|
+
as ||= info.input
|
|
52
|
+
|
|
53
|
+
options[:placeholder] = info.placeholder unless options.key?(:placeholder)
|
|
54
|
+
options[:autocomplete] = info.autocomplete unless options.key?(:autocomplete)
|
|
55
|
+
|
|
56
|
+
public_send(as, method, *args, **options)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def hint(method, text = info(method).hint, **)
|
|
60
|
+
@template.tag.small(text, **) if text.present?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def errors(method)
|
|
64
|
+
errors = object.try(:errors)
|
|
65
|
+
associated = info(method).reflection&.then { errors[_1.name] } || []
|
|
66
|
+
errors[method] | associated
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def error_text(method)
|
|
70
|
+
errors(method).to_sentence.presence
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def error(method, text = error_text(method), **)
|
|
74
|
+
@template.tag.span(text, **) if text.present?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fields_for(method, object = object_for(method), **, &)
|
|
78
|
+
method = method.to_s.chomp("_attributes").to_sym
|
|
79
|
+
if block_given?
|
|
80
|
+
super
|
|
81
|
+
else
|
|
82
|
+
super(method, **) do |f|
|
|
83
|
+
f.remaining_fields
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def object_for(method)
|
|
89
|
+
method = method.to_s.chomp("_attributes").to_sym
|
|
90
|
+
if info(method).reflection.collection?
|
|
91
|
+
object.try(method)
|
|
92
|
+
else
|
|
93
|
+
object.try(method) || object.try("build_#{method}")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def remaining_attributes = policy.editable_attributes - @fields.keys
|
|
98
|
+
|
|
99
|
+
def remaining_fields(**, &block)
|
|
100
|
+
block ||= proc { field(_1, **) }
|
|
101
|
+
fields = remaining_attributes.map(&block)
|
|
102
|
+
@template.safe_join(fields)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "rails/generators/active_record/migration"
|
|
2
|
+
require "rails/generators/bundle_helper"
|
|
3
|
+
|
|
4
|
+
module CafeCar::Generators
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
include ActiveRecord::Generators::Migration
|
|
7
|
+
include Rails::Generators::BundleHelper
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def migration(name, ...) = migration_template("#{name}.rb", "db/migrate/#{name}.rb", ...)
|
|
12
|
+
|
|
13
|
+
def model(name, ...)
|
|
14
|
+
name = name.chomp(".rb").underscore
|
|
15
|
+
template("#{name}.rb", "app/models/#{name}.rb", ...)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def class_namespace = class_path.join("/").classify
|
|
19
|
+
|
|
20
|
+
def module_namespacing(&block)
|
|
21
|
+
super { concat wrap_with_module(class_namespace, &block) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def wrap_with_module(module_name, &block)
|
|
25
|
+
content = capture(&block)
|
|
26
|
+
return content if module_name.blank?
|
|
27
|
+
content = indent(content).chomp
|
|
28
|
+
"module #{module_name}\n#{content}\nend\n"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
module Helpers
|
|
3
|
+
# Returns a new `Context`. Used for instantiating components: `ui.Button(:primary, "Submit")`
|
|
4
|
+
def ui(*args, **, &)
|
|
5
|
+
# For now, this must be defined in a helper instead of in the controller. Passing `view_context` or `helpers`
|
|
6
|
+
# from the controller somehow breaks `capture`. `capture` will return the captured content, but the content
|
|
7
|
+
# _also_ gets appended to the original output buffer.
|
|
8
|
+
# This can be tested in a view by comparing the behavior of `= capture do` with
|
|
9
|
+
# `= controller.view_context.capture do`; the latter outputs the content twice.
|
|
10
|
+
if args.any?
|
|
11
|
+
present(*args, **, &)
|
|
12
|
+
elsif block_given?
|
|
13
|
+
capture(&)
|
|
14
|
+
else
|
|
15
|
+
@ui ||= CafeCar::Context.new(self)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ui_class(names, *args, **opts)
|
|
20
|
+
names = [ *names ].map(&:camelize)
|
|
21
|
+
name = names.join("_")
|
|
22
|
+
args.flatten!
|
|
23
|
+
args.compact_blank!
|
|
24
|
+
opts.compact_blank!
|
|
25
|
+
opts.merge!(*args.extract!(Hash))
|
|
26
|
+
|
|
27
|
+
flags = args.extract!(Symbol)
|
|
28
|
+
flags |= opts.extract_if! { _1.is_a? Symbol }.keys
|
|
29
|
+
flags.map! { "#{name}-#{_1}" }
|
|
30
|
+
|
|
31
|
+
[ *name, *flags, *args, *opts.keys ].join(" ")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def body_classes = [ *controller_path.split(?/), action_name, *@body_class ]
|
|
35
|
+
|
|
36
|
+
def title(object)
|
|
37
|
+
present(object).title.presence.tap do |title|
|
|
38
|
+
content_for(:title, title)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cat(*args)
|
|
43
|
+
args.flatten.each do |arg|
|
|
44
|
+
arg = capture(&arg) if arg.respond_to?(:to_proc)
|
|
45
|
+
arg = arg.to_s
|
|
46
|
+
concat(arg) if arg.present?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cap(*)
|
|
51
|
+
capture { cat(*) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def capture(*, &)
|
|
55
|
+
super do
|
|
56
|
+
yield(*).then { _1.try(:html_safe?) ? _1.to_s : _1 }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def present(*args, **options)
|
|
61
|
+
@presenters ||= {}
|
|
62
|
+
@presenters[[ args, options ]] ||= CafeCar[:Presenter].present(self, *args, **options)
|
|
63
|
+
end
|
|
64
|
+
alias_method :p, :present
|
|
65
|
+
|
|
66
|
+
def current_href?(*, check_parameters: false, **) = current_page?(href_for(*, **), check_parameters:)
|
|
67
|
+
def ancestor_href?(...) = URI(href_for(...)) < URI(url_for(request.url))
|
|
68
|
+
|
|
69
|
+
def href_for(*parts, namespace: self.namespace, **params)
|
|
70
|
+
HrefBuilder.new(*parts, namespace:, template: self, **params).to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def view_url(view)
|
|
74
|
+
view = view.to_s
|
|
75
|
+
params = request.params.merge(view:)
|
|
76
|
+
params.delete(:view) if params[:view] == default_view
|
|
77
|
+
url_for(params)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def context(name = nil, &)
|
|
81
|
+
@context ||= []
|
|
82
|
+
|
|
83
|
+
if block_given?
|
|
84
|
+
@context << name
|
|
85
|
+
r = capture(&)
|
|
86
|
+
@context.pop
|
|
87
|
+
r
|
|
88
|
+
else
|
|
89
|
+
@context
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def context?(*names)
|
|
94
|
+
context.reverse_each do |ctx|
|
|
95
|
+
return true if names.empty?
|
|
96
|
+
names.pop if ctx == names.last
|
|
97
|
+
end
|
|
98
|
+
names.empty?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def link(object)
|
|
102
|
+
@links ||= {}
|
|
103
|
+
@links[object] ||= CafeCar[:LinkBuilder].new(self, object)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def link_to(...)
|
|
107
|
+
raise ArgumentError, "Links cannot be nested" if context?(:a)
|
|
108
|
+
context(:a) { super }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def icon(name = nil, *, **, &)
|
|
112
|
+
case name
|
|
113
|
+
when Symbol
|
|
114
|
+
class_ = name&.then { "iconoir-#{_1.to_s.dasherize}" }
|
|
115
|
+
when String, Array
|
|
116
|
+
label = name
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
ui.Icon(*label, *, tag: :i, class: class_, **, &)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def breadcrumbs(*items)
|
|
123
|
+
ui.Row safe_join(items.compact_blank, icon(:nav_arrow_right, :dim))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def filter_form_for(objects, **options, &block)
|
|
127
|
+
raise ArgumentError, "First argument to filter_form_for cannot be nil" if objects.nil?
|
|
128
|
+
|
|
129
|
+
form_for CafeCar[:FilterBuilder].new(objects, parsed_params),
|
|
130
|
+
builder: CafeCar["Filter::FormBuilder"],
|
|
131
|
+
method: :get,
|
|
132
|
+
url: "",
|
|
133
|
+
as: "",
|
|
134
|
+
**options,
|
|
135
|
+
&block
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def table_for(objects, **options, &block)
|
|
139
|
+
CafeCar[:TableBuilder].new(self, objects:, **options, &block)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def debug? = params.key?(:debug)
|
|
143
|
+
def console? = params.key?(:console)
|
|
144
|
+
|
|
145
|
+
def comment(text)
|
|
146
|
+
"<!-- #{text} -->".html_safe
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def partial?(path)
|
|
150
|
+
prefixes = path.include?(?/) ? [] : lookup_context.prefixes
|
|
151
|
+
lookup_context.any?(path, prefixes, true)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def get_partial(path)
|
|
155
|
+
prefixes = path.include?(?/) ? [] : lookup_context.prefixes
|
|
156
|
+
lookup_context.find(path, prefixes, true)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def template_glob(glob)
|
|
160
|
+
lookup_context.view_paths
|
|
161
|
+
.flat_map { _1.send(:template_glob, glob) }
|
|
162
|
+
.map { ActionView::TemplatePath.parse(_1) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def navigation
|
|
166
|
+
@navigation ||= CafeCar::Navigation.new(self)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def namespace
|
|
170
|
+
@namespace ||= controller_path.split("/").tap(&:pop).map(&:to_sym)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def method_missing(name, ...)
|
|
174
|
+
return ui.send(name, ...) if name =~ /^[A-Z]/
|
|
175
|
+
super
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class HrefBuilder
|
|
3
|
+
attr_reader :parts, :namespace
|
|
4
|
+
|
|
5
|
+
def initialize(*parts, namespace: [], template: nil, **params)
|
|
6
|
+
@params = params
|
|
7
|
+
@parts = parts
|
|
8
|
+
@namespace = namespace
|
|
9
|
+
@template = template
|
|
10
|
+
normalize!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_s
|
|
14
|
+
case [ *@parts ]
|
|
15
|
+
in [ String ]
|
|
16
|
+
[ *@parts, *@params.to_query.presence ].join(??)
|
|
17
|
+
in []
|
|
18
|
+
@template.url_for(@params)
|
|
19
|
+
else
|
|
20
|
+
namespace = collapsed_namespace
|
|
21
|
+
parts = @parts.map { singular_resource(_1) }
|
|
22
|
+
begin
|
|
23
|
+
@template.url_for([ *namespace, *parts, @params ])
|
|
24
|
+
rescue NoMethodError
|
|
25
|
+
raise if namespace.empty?
|
|
26
|
+
namespace.pop
|
|
27
|
+
retry
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def normalize!
|
|
33
|
+
@parts.flatten!
|
|
34
|
+
@params.with_defaults!(@parts.extract_options!)
|
|
35
|
+
@namespace = @namespace.underscore.split(?/).map(&:to_sym) if @namespace.is_a? String
|
|
36
|
+
@params.delete(:action) if @params[:action].in? %i[show destroy index]
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def collapsed_namespace
|
|
41
|
+
0.upto(@namespace.size) do |i|
|
|
42
|
+
if parts_start_with? @namespace.drop(i)
|
|
43
|
+
return @namespace.slice(0, i)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def expanded_parts
|
|
49
|
+
@expanded_parts ||= @parts.flat_map { expand_part _1 }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Records route polymorphically via the plural `route_key`. For a singular
|
|
55
|
+
# resource (`resource :session`) that helper doesn't exist, so fall back to
|
|
56
|
+
# the singular route key. Leaves non-records (symbols, strings) untouched.
|
|
57
|
+
def singular_resource(part)
|
|
58
|
+
name = model_name_for(part) or return part
|
|
59
|
+
return part if @template.respond_to?("#{name.route_key}_path")
|
|
60
|
+
|
|
61
|
+
@template.respond_to?("#{name.singular_route_key}_path") ?
|
|
62
|
+
name.singular_route_key.to_sym : part
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def model_name_for(part)
|
|
66
|
+
klass = part.is_a?(Module) ? part : part.class
|
|
67
|
+
klass.model_name if klass.respond_to?(:model_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def expand_part(part)
|
|
71
|
+
normalize case part
|
|
72
|
+
when Symbol, String, Hash, Array then part
|
|
73
|
+
when ActiveModel::Naming then part.model_name.collection
|
|
74
|
+
when Class then part.name.underscore
|
|
75
|
+
else expand_part(part.class)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def normalize(part)
|
|
80
|
+
case part
|
|
81
|
+
when String
|
|
82
|
+
part.split(?/).map(&:to_sym)
|
|
83
|
+
else part
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def parts_start_with?(prefix)
|
|
88
|
+
prefix.zip(expanded_parts).all? { _1 == _2 }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# def href(*parts, **params)
|
|
94
|
+
# params.merge! parts.extract_options!
|
|
95
|
+
# params.delete(:action) if %i[show destroy index].include? params[:action]
|
|
96
|
+
# url_for([*namespace, *parts, params])
|
|
97
|
+
# end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module CafeCar
|
|
2
|
+
class InputBuilder
|
|
3
|
+
attr_reader :form, :method, :as
|
|
4
|
+
|
|
5
|
+
delegate :object, :object_name, to: :form
|
|
6
|
+
|
|
7
|
+
def initialize(method:, form:, **options, &block)
|
|
8
|
+
@method = method
|
|
9
|
+
@form = form
|
|
10
|
+
@options = options
|
|
11
|
+
@block = block
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def type
|
|
15
|
+
@type ||=
|
|
16
|
+
object.type_for_attribute(@method)&.type ||
|
|
17
|
+
reflect_on(method)&.macro
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def placeholder
|
|
21
|
+
I18n.t(method, scope: [ :helpers, :placeholder, object_name ], raise: true).presence
|
|
22
|
+
rescue I18n::MissingTranslationData => _
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|