tramway 2.3.1.4 → 3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +47 -15
- data/app/components/tailwind_component.rb +7 -1
- data/app/components/tramway/chats/message_component.rb +1 -1
- data/app/components/tramway/comments/show_path_component.html.haml +4 -0
- data/app/components/tramway/comments/show_path_component.rb +11 -0
- data/app/components/tramway/form/builder.rb +21 -2
- data/app/components/tramway/form/checkbox_component.html.haml +1 -1
- data/app/components/tramway/form/date_field_component.html.haml +3 -3
- data/app/components/tramway/form/datetime_field_component.html.haml +1 -1
- data/app/components/tramway/form/file_field_component.html.haml +1 -1
- data/app/components/tramway/form/multiselect_component.html.haml +1 -1
- data/app/components/tramway/form/number_field_component.html.haml +1 -1
- data/app/components/tramway/form/select_component.html.haml +1 -1
- data/app/components/tramway/form/text_area_component.html.haml +1 -1
- data/app/components/tramway/form/text_field_component.html.haml +1 -1
- data/app/components/tramway/form/time_field_component.html.haml +6 -0
- data/app/components/tramway/form/time_field_component.rb +9 -0
- data/app/controllers/tramway/entities_controller.rb +43 -8
- data/app/views/kaminari/_paginator.html.haml +1 -2
- data/app/views/tramway/entities/_form.html.haml +5 -1
- data/app/views/tramway/entities/_list.html.haml +16 -11
- data/app/views/tramway/entities/show.html.haml +38 -18
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +2 -2
- data/config/tailwind.config.js +2 -0
- data/docs/AGENTS.md +70 -3
- data/lib/tramway/config.rb +0 -1
- data/lib/tramway/configs/entities/page.rb +1 -0
- data/lib/tramway/engine.rb +9 -1
- data/lib/tramway/helpers/views_helper.rb +3 -1
- data/lib/tramway/searchable.rb +43 -0
- data/lib/tramway/utils/field.rb +2 -2
- data/lib/tramway/version.rb +1 -1
- data/lib/tramway/warnings.rb +17 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6dc5de667eeeb4db78705dc65c49743449c5c14ef4bb2fe45159fd8a5abd209b
|
|
4
|
+
data.tar.gz: fdb10f35b0ed3e6b20fc048044418a5b78a846bc4c4383ff6090981a884df325
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 562e93acbe9f809cd7b7bbbedb2b4a860be4763b9aa2eb709b6b4cce904804303db6d7c652d2b591468c4d3fc72ac44df19d7251a1829f94a9a54ef92c494eb6
|
|
7
|
+
data.tar.gz: ec1b87e02459067629e2432cd938ef80d639398a06861700b0ee0cc6f854cb83760fffc85dd4488e8bc37303249cc71580dc0d186a3ec380a5bc083296584b64
|
data/README.md
CHANGED
|
@@ -194,6 +194,30 @@ end
|
|
|
194
194
|
In this example, the `Campaign` entity will display only records returned by the `active` scope on its index page, while all
|
|
195
195
|
other pages continue to show every record unless another scope is specified.
|
|
196
196
|
|
|
197
|
+
**search**
|
|
198
|
+
|
|
199
|
+
Search is disabled by default on index pages. Enable it per-entity by setting `search: true` on the `:index` page entry:
|
|
200
|
+
|
|
201
|
+
*config/initializers/tramway.rb*
|
|
202
|
+
```ruby
|
|
203
|
+
Tramway.configure do |config|
|
|
204
|
+
config.entities = [
|
|
205
|
+
{
|
|
206
|
+
name: :campaign,
|
|
207
|
+
pages: [
|
|
208
|
+
{
|
|
209
|
+
action: :index,
|
|
210
|
+
search: true
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
When search is enabled, Tramway uses `Model.search(query)` if defined. If not, it falls back to `Model.tramway_search(query)` and logs a warning.
|
|
219
|
+
The fallback is generic and not tailored to your data structure, so it is not intended for long-term use and may be slow or not scalable.
|
|
220
|
+
|
|
197
221
|
**show page**
|
|
198
222
|
|
|
199
223
|
To render a show page for an entity, declare a `:show` action inside the `pages` array in
|
|
@@ -267,13 +291,13 @@ end
|
|
|
267
291
|
|
|
268
292
|
**fields method**
|
|
269
293
|
|
|
270
|
-
Use `
|
|
294
|
+
Use `fields` in your form class to customize which form helpers get rendered and which options are passed to them.
|
|
271
295
|
Each field must map to a form helper method name. When you need to pass options, use a hash where `:type` is the helper
|
|
272
296
|
method name and the remaining keys are passed as named arguments.
|
|
273
297
|
|
|
274
298
|
```ruby
|
|
275
299
|
class UserForm < Tramway::BaseForm
|
|
276
|
-
properties :email, :about_me, :user_type
|
|
300
|
+
properties :email, :about_me, :user_type, :score
|
|
277
301
|
|
|
278
302
|
fields email: :email,
|
|
279
303
|
name: :text,
|
|
@@ -284,7 +308,15 @@ class UserForm < Tramway::BaseForm
|
|
|
284
308
|
user_type: {
|
|
285
309
|
type: :select,
|
|
286
310
|
collection: ['regular', 'user']
|
|
311
|
+
},
|
|
312
|
+
score: {
|
|
313
|
+
type: :number,
|
|
314
|
+
value: -> (object) { Score.find_by(user_id: object.id).value }
|
|
287
315
|
}
|
|
316
|
+
|
|
317
|
+
def score=(value)
|
|
318
|
+
Score.find_by(user_id: object.id).update(value:)
|
|
319
|
+
end
|
|
288
320
|
end
|
|
289
321
|
```
|
|
290
322
|
|
|
@@ -1091,14 +1123,27 @@ within the form will use the same size value.
|
|
|
1091
1123
|
<% end %>
|
|
1092
1124
|
```
|
|
1093
1125
|
|
|
1126
|
+
Use `horizontal: true` to render a horizontal form layout.
|
|
1127
|
+
|
|
1128
|
+
```erb
|
|
1129
|
+
<%= tramway_form_for @user, horizontal: true do |f| %>
|
|
1130
|
+
<%= f.text_field :text %>
|
|
1131
|
+
<%= f.submit 'Create User' %>
|
|
1132
|
+
<% end %>
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1094
1135
|
Available form helpers:
|
|
1095
1136
|
* text_field
|
|
1096
1137
|
* email_field
|
|
1138
|
+
* number_field
|
|
1139
|
+
* text_area
|
|
1097
1140
|
* password_field
|
|
1098
1141
|
* file_field
|
|
1142
|
+
* check_box
|
|
1099
1143
|
* select
|
|
1100
1144
|
* date_field
|
|
1101
1145
|
* datetime_field
|
|
1146
|
+
* time_field
|
|
1102
1147
|
* multiselect ([Stimulus-based](https://github.com/Purple-Magic/tramway#stimulus-based-inputs))
|
|
1103
1148
|
* submit
|
|
1104
1149
|
|
|
@@ -1177,19 +1222,6 @@ Tramway uses [Tailwind](https://tailwindcss.com/) by default. It has tailwind-st
|
|
|
1177
1222
|
|
|
1178
1223
|
#### How to use
|
|
1179
1224
|
|
|
1180
|
-
*Gemfile*
|
|
1181
|
-
```ruby
|
|
1182
|
-
gem 'tramway'
|
|
1183
|
-
gem 'kaminari'
|
|
1184
|
-
```
|
|
1185
|
-
|
|
1186
|
-
*config/initializers/tramway.rb*
|
|
1187
|
-
```ruby
|
|
1188
|
-
Tramway.configure do |config|
|
|
1189
|
-
config.pagination = { enabled: true } # enabled is false by default
|
|
1190
|
-
end
|
|
1191
|
-
```
|
|
1192
|
-
|
|
1193
1225
|
*app/views/users/index.html.erb*
|
|
1194
1226
|
```erb
|
|
1195
1227
|
<%= paginate @users %> <%# it will render tailwind-styled pagination buttons by default %>
|
|
@@ -58,7 +58,7 @@ class TailwindComponent < Tramway::BaseComponent
|
|
|
58
58
|
|
|
59
59
|
def file_button_base_classes
|
|
60
60
|
theme_classes(
|
|
61
|
-
classic: 'inline-block text-
|
|
61
|
+
classic: 'inline-block text-white font-semibold rounded-xl cursor-pointer mt-4 bg-blue-600 ' \
|
|
62
62
|
'hover:bg-blue-800 shadow-md'
|
|
63
63
|
)
|
|
64
64
|
end
|
|
@@ -83,6 +83,12 @@ class TailwindComponent < Tramway::BaseComponent
|
|
|
83
83
|
)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
def default_container_classes
|
|
87
|
+
return if options[:horizontal]
|
|
88
|
+
|
|
89
|
+
'mb-4'
|
|
90
|
+
end
|
|
91
|
+
|
|
86
92
|
def size_class(key)
|
|
87
93
|
size_classes = SIZE_CLASSES.fetch(size) { SIZE_CLASSES[:medium] }
|
|
88
94
|
size_classes.fetch(key) { SIZE_CLASSES[:medium].fetch(key) }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
module Comments
|
|
5
|
+
# HTML comment that shows in case `show_path` is needed but it's empty
|
|
6
|
+
# It does not show for Rails.env.production
|
|
7
|
+
class ShowPathComponent < Tramway::BaseComponent
|
|
8
|
+
option :decorator_class
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -11,9 +11,14 @@ module Tramway
|
|
|
11
11
|
include Tramway::ColorsMethods
|
|
12
12
|
|
|
13
13
|
def initialize(object_name, object, template, options)
|
|
14
|
+
@horizontal = options[:horizontal] || false
|
|
15
|
+
|
|
16
|
+
options.merge!(class: [options[:class], 'flex flex-row items-center gap-2'].compact.join(' ')) if @horizontal
|
|
17
|
+
|
|
14
18
|
super
|
|
15
19
|
|
|
16
20
|
@form_size = options[:size] || options['size'] || :medium
|
|
21
|
+
@form_object_class = options[:form_object_class]
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
def common_field(component_name, input_method, attribute, **options, &)
|
|
@@ -62,6 +67,10 @@ module Tramway
|
|
|
62
67
|
common_field(:datetime_field, :datetime_field, attribute, **, &)
|
|
63
68
|
end
|
|
64
69
|
|
|
70
|
+
def time_field(attribute, **, &)
|
|
71
|
+
common_field(:time_field, :time_field, attribute, **, &)
|
|
72
|
+
end
|
|
73
|
+
|
|
65
74
|
def file_field(attribute, **options, &)
|
|
66
75
|
sanitized_options = sanitize_options(options)
|
|
67
76
|
input = super(attribute, **sanitized_options.merge(class: :hidden))
|
|
@@ -118,12 +127,22 @@ module Tramway
|
|
|
118
127
|
unbound_method.bind(self)
|
|
119
128
|
end
|
|
120
129
|
|
|
130
|
+
def form_object
|
|
131
|
+
@form_object_class&.new object
|
|
132
|
+
end
|
|
133
|
+
|
|
121
134
|
def get_value(attribute, options)
|
|
122
|
-
options[:value] || object.presence&.public_send(attribute)
|
|
135
|
+
options[:value] || form_object&.public_send(attribute).presence || object.presence&.public_send(attribute)
|
|
123
136
|
end
|
|
124
137
|
|
|
125
138
|
def default_options(attribute, options)
|
|
126
|
-
{
|
|
139
|
+
{
|
|
140
|
+
attribute:,
|
|
141
|
+
label: label_build(attribute, options),
|
|
142
|
+
for: for_id(attribute),
|
|
143
|
+
options: options.merge(horizontal: @horizontal),
|
|
144
|
+
size: form_size
|
|
145
|
+
}
|
|
127
146
|
end
|
|
128
147
|
|
|
129
148
|
def label_build(attribute, options)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
%div{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
3
|
= component('tramway/form/label', for: @for) do
|
|
4
4
|
= @label
|
|
5
|
-
- classes = "#{size_class(:text_input)} #{text_input_base_classes}"
|
|
6
|
-
= @input.call @attribute, **@options.merge(class: classes), value: @value
|
|
5
|
+
- classes = "#{size_class(:text_input)} #{text_input_base_classes}"
|
|
6
|
+
= @input.call @attribute, **@options.merge(class: classes), value: @value
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'tramway/warnings'
|
|
4
|
+
|
|
3
5
|
module Tramway
|
|
4
6
|
# Main controller for entities pages
|
|
5
7
|
class EntitiesController < Tramway.config.application_controller.constantize
|
|
@@ -15,8 +17,10 @@ module Tramway
|
|
|
15
17
|
model_class.public_send(index_scope)
|
|
16
18
|
else
|
|
17
19
|
model_class.order(id: :desc)
|
|
18
|
-
end
|
|
20
|
+
end => entities
|
|
19
21
|
|
|
22
|
+
entities = search(entities)
|
|
23
|
+
entities = entities.page(params[:page])
|
|
20
24
|
@entities = entities
|
|
21
25
|
|
|
22
26
|
@namespace = entity.namespace
|
|
@@ -84,18 +88,49 @@ module Tramway
|
|
|
84
88
|
end
|
|
85
89
|
|
|
86
90
|
def set_associations
|
|
87
|
-
@associations = @record.show_associations.map do |association|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
records = Kaminari.paginate_array(@record.public_send(association.name)).page(params[:page])
|
|
91
|
+
@associations = @record.show_associations.map do |(association, options)|
|
|
92
|
+
options ||= {}
|
|
93
|
+
association_type = @record.object.class.reflect_on_association(association).macro
|
|
91
94
|
|
|
92
95
|
{
|
|
93
96
|
name: association,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
model_class: records.first.object.class
|
|
97
|
+
association_type:,
|
|
98
|
+
**send("#{association_type}_associations", association, options)
|
|
97
99
|
}
|
|
98
100
|
end.compact
|
|
99
101
|
end
|
|
102
|
+
|
|
103
|
+
# rubocop:disable Naming/PredicatePrefix
|
|
104
|
+
def has_many_associations(association, options)
|
|
105
|
+
records = Kaminari.paginate_array(@record.public_send(association.name)).page(params[:page])
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
decorator: records.first&.class,
|
|
109
|
+
records:,
|
|
110
|
+
model_class: records.first&.object&.class,
|
|
111
|
+
new_record_path: options[:new_record_path]
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
# rubocop:enable Naming/PredicatePrefix
|
|
115
|
+
|
|
116
|
+
def belongs_to_associations(association, _options)
|
|
117
|
+
record = @record.public_send(association.name)
|
|
118
|
+
|
|
119
|
+
{ decorator: record.class, record:, model_class: record.class }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def search(entities)
|
|
123
|
+
query = params[:query]
|
|
124
|
+
|
|
125
|
+
if entity.page(:index).search && query.present?
|
|
126
|
+
return entities.search(query) if entities.respond_to?(:search)
|
|
127
|
+
|
|
128
|
+
Tramway::Warnings.search_fallback model_class
|
|
129
|
+
|
|
130
|
+
return entities.tramway_search(query)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
entities
|
|
134
|
+
end
|
|
100
135
|
end
|
|
101
136
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
= paginator.render do
|
|
2
|
-
%nav.pagination.flex.items-center.justify-center.
|
|
2
|
+
%nav.pagination.flex.items-center.justify-center.gap-x-1
|
|
3
3
|
= first_page_tag unless current_page.first?
|
|
4
4
|
= prev_page_tag unless current_page.first?
|
|
5
5
|
- each_page do |page|
|
|
@@ -9,4 +9,3 @@
|
|
|
9
9
|
= gap_tag
|
|
10
10
|
= next_page_tag unless current_page.last?
|
|
11
11
|
= last_page_tag unless current_page.last?
|
|
12
|
-
|
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
- @record.errors.full_messages.each do |message|
|
|
11
11
|
%li= message
|
|
12
12
|
|
|
13
|
+
- prepolulated_values = params[@entity.name.to_param]&.compact || {}
|
|
14
|
+
|
|
13
15
|
- @record.class.fields.each do |(attribute, field_type)|
|
|
14
|
-
= f.tramway_field field_type, attribute
|
|
16
|
+
= f.tramway_field field_type, attribute,
|
|
17
|
+
value: prepolulated_values&.dig(attribute),
|
|
18
|
+
label: @model_class.human_attribute_name(attribute)
|
|
15
19
|
|
|
16
20
|
.flex.flex-row.space-x-2
|
|
17
21
|
= f.submit t('tramway.actions.save')
|
|
@@ -10,15 +10,21 @@
|
|
|
10
10
|
= tramway_title do
|
|
11
11
|
= content_for :title
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
path: Tramway::Engine.routes.url_helpers.public_send(@entity.routes.new),
|
|
19
|
-
type: :will
|
|
13
|
+
= component 'tramway/actions_buttons_container' do
|
|
14
|
+
- if @entity.page(:create).present?
|
|
15
|
+
= tramway_button text: t('tramway.actions.new'),
|
|
16
|
+
path: Tramway::Engine.routes.url_helpers.public_send(@entity.routes.new),
|
|
17
|
+
type: :will
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
= decorator.constantize.index_header_content.call(@entities) if decorator.constantize.index_header_content.present?
|
|
20
|
+
|
|
21
|
+
.flex.flex-row.justify-end.mt-2.gap-4
|
|
22
|
+
- if @entity.page(:index).search
|
|
23
|
+
= form_with url: public_send(custom_path_method), method: :get, local: true, builder: Tramway::Form::Builder, horizontal: true do |f|
|
|
24
|
+
= f.text_field :query, label: false, value: params[:query], placeholder: t('tramway.actions.search')
|
|
25
|
+
= f.submit t('tramway.actions.search')
|
|
26
|
+
|
|
27
|
+
= paginate @entities, custom_path_method:
|
|
22
28
|
|
|
23
29
|
- if index_attributes.empty?
|
|
24
30
|
%p.text-center.mt-10
|
|
@@ -30,6 +36,5 @@
|
|
|
30
36
|
- @entities.each do |item|
|
|
31
37
|
= component 'tramway/entity', entity: @entity, item: item
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
= paginate @entities, custom_path_method:
|
|
39
|
+
.flex.mt-4
|
|
40
|
+
= paginate @entities, custom_path_method:
|
|
@@ -20,11 +20,20 @@
|
|
|
20
20
|
= @record.show_header_content
|
|
21
21
|
|
|
22
22
|
- if @record.show_attributes.empty?
|
|
23
|
-
%p.text-center.mt-10
|
|
23
|
+
%p.text-center.mt-10.text-white
|
|
24
24
|
You should fill object-level method `show_attributes` inside your
|
|
25
25
|
= @record.class.name
|
|
26
26
|
|
|
27
27
|
= tramway_table class: 'mt-4' do
|
|
28
|
+
- @associations.select { _1[:association_type] == :belongs_to }.each do |association|
|
|
29
|
+
- if association[:record].show_path.blank?
|
|
30
|
+
= component 'tramway/comments/show_path', decorator_class: association[:record].class.name
|
|
31
|
+
= tramway_row href: association[:record].show_path do
|
|
32
|
+
= tramway_cell do
|
|
33
|
+
= @model_class.human_attribute_name(association[:name])
|
|
34
|
+
= tramway_cell do
|
|
35
|
+
= association[:record].title
|
|
36
|
+
|
|
28
37
|
- @record.show_attributes.each do |attribute|
|
|
29
38
|
= tramway_row do
|
|
30
39
|
= tramway_cell do
|
|
@@ -32,26 +41,37 @@
|
|
|
32
41
|
= tramway_cell do
|
|
33
42
|
= @record.public_send(attribute)
|
|
34
43
|
|
|
35
|
-
- @associations.each do |association|
|
|
36
|
-
.flex.flex-row.justify-between.mt-8
|
|
44
|
+
- @associations.select { _1[:association_type] == :has_many }.each do |association|
|
|
45
|
+
.flex.flex-row.justify-between.mt-8.text-white
|
|
37
46
|
%h3.text-2xl.font-bold
|
|
38
47
|
= @model_class.human_attribute_name(association[:name])
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
- if association[:records].any?
|
|
50
|
+
%div
|
|
51
|
+
= paginate association[:records],
|
|
52
|
+
custom_path_method: @entity.index_helper_method,
|
|
53
|
+
custom_path_arguments: [@record.id]
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
= tramway_button text: t('tramway.actions.new'),
|
|
56
|
+
path: association[:new_record_path],
|
|
57
|
+
type: :will
|
|
58
|
+
|
|
59
|
+
- if association[:records].any?
|
|
60
|
+
= tramway_table class: 'mt-4' do
|
|
61
|
+
= tramway_header headers: association[:decorator].table_headers(model_class: association[:model_class])
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
- association[:records].each do |record|
|
|
64
|
+
- if record.show_path.blank?
|
|
65
|
+
= component 'tramway/comments/show_path', decorator_class: association[:decorator].name
|
|
66
|
+
= tramway_row href: record.show_path do
|
|
67
|
+
- if association[:decorator].index_attributes.empty?
|
|
68
|
+
.text-white
|
|
69
|
+
You should set `index_attributes` method in #{association[:decorator].name} to display the table of associated records.
|
|
70
|
+
- association[:decorator].index_attributes.each do |column|
|
|
71
|
+
= tramway_cell do
|
|
72
|
+
= record.public_send(column)
|
|
53
73
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
.mt-4
|
|
75
|
+
= paginate association[:records],
|
|
76
|
+
custom_path_method: @entity.index_helper_method,
|
|
77
|
+
custom_path_arguments: [@record.id]
|
data/config/locales/en.yml
CHANGED
data/config/routes.rb
CHANGED
|
@@ -14,7 +14,7 @@ Tramway::Engine.routes.draw do
|
|
|
14
14
|
resource_name = segments.pop
|
|
15
15
|
|
|
16
16
|
define_resource = proc do
|
|
17
|
-
|
|
17
|
+
entity.pages.reduce([]) do |acc, page|
|
|
18
18
|
case page.action
|
|
19
19
|
when 'index'
|
|
20
20
|
acc << :index
|
|
@@ -29,7 +29,7 @@ Tramway::Engine.routes.draw do
|
|
|
29
29
|
else
|
|
30
30
|
acc
|
|
31
31
|
end
|
|
32
|
-
end
|
|
32
|
+
end => actions
|
|
33
33
|
|
|
34
34
|
resources resource_name.pluralize.to_sym,
|
|
35
35
|
only: actions.map(&:to_sym),
|
data/config/tailwind.config.js
CHANGED
|
@@ -195,6 +195,7 @@ module.exports = {
|
|
|
195
195
|
|
|
196
196
|
// === Pagination styles ===
|
|
197
197
|
'hover:bg-gray-300',
|
|
198
|
+
'gap-x-1',
|
|
198
199
|
|
|
199
200
|
// === Dark mode pagination styles ===
|
|
200
201
|
'bg-gray-900',
|
|
@@ -331,6 +332,7 @@ module.exports = {
|
|
|
331
332
|
'border-red-600',
|
|
332
333
|
'bg-red-100',
|
|
333
334
|
'text-red-800',
|
|
335
|
+
'space-x-2',
|
|
334
336
|
|
|
335
337
|
// === Multiselect dropdown positioning ===
|
|
336
338
|
'absolute',
|
data/docs/AGENTS.md
CHANGED
|
@@ -105,6 +105,28 @@ end
|
|
|
105
105
|
|
|
106
106
|
If admin panel requested to be implemented from scratch, do the same with `namespace: :admin`
|
|
107
107
|
|
|
108
|
+
### Rule 1.1
|
|
109
|
+
Search is disabled by default on index pages. Enable it with `search: true` on the `:index` page definition:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
Tramway.configure do |config|
|
|
113
|
+
config.entities = [
|
|
114
|
+
{
|
|
115
|
+
name: :participant,
|
|
116
|
+
pages: [
|
|
117
|
+
{
|
|
118
|
+
action: :index,
|
|
119
|
+
search: true
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If `Model.search` exists, Tramway uses it. Otherwise it falls back to `Model.tramway_search` and logs a warning.
|
|
128
|
+
The fallback is generic, not tailored to the data structure, and should not be used long-term because it may be slow or not scalable.
|
|
129
|
+
|
|
108
130
|
### Rule 2
|
|
109
131
|
Normalize input with `normalizes` (from Tramway) for attributes like email, phone, etc. Don't use `normalizes` in model unless it requested explicitly.
|
|
110
132
|
|
|
@@ -133,6 +155,21 @@ Use Tramway Button for buttons. Always add a color of the button via `color:` or
|
|
|
133
155
|
### Rule 7
|
|
134
156
|
Use `tramway_form_for` instead `form_with`, `form_for`
|
|
135
157
|
|
|
158
|
+
Available `tramway_form_for` helpers:
|
|
159
|
+
- `text_field`
|
|
160
|
+
- `email_field`
|
|
161
|
+
- `number_field`
|
|
162
|
+
- `text_area`
|
|
163
|
+
- `password_field`
|
|
164
|
+
- `file_field`
|
|
165
|
+
- `check_box`
|
|
166
|
+
- `select`
|
|
167
|
+
- `date_field`
|
|
168
|
+
- `datetime_field`
|
|
169
|
+
- `time_field`
|
|
170
|
+
- `multiselect`
|
|
171
|
+
- `submit`
|
|
172
|
+
|
|
136
173
|
### Rule 8
|
|
137
174
|
Inherit all components from Tramway::BaseComponent
|
|
138
175
|
|
|
@@ -150,21 +187,32 @@ it raises `ArgumentError`. `chat_id` must match the stream id used in `tramway_c
|
|
|
150
187
|
### Rule 9
|
|
151
188
|
If page `create` or `update` is configured for an entity, use Tramway Form pattern for forms. Visible fields are configured via `form_fields` method.
|
|
152
189
|
|
|
153
|
-
Use
|
|
190
|
+
Use `fields` in your form class to customize which form helpers get rendered and which options are passed to them. Each field must map to a form helper method name. When you need to pass options, use a hash where :type is the helper method name and the remaining keys are passed as named arguments.
|
|
154
191
|
|
|
155
192
|
Example:
|
|
156
193
|
|
|
157
|
-
*app/forms/user_form.rb*:
|
|
158
194
|
```ruby
|
|
159
195
|
class UserForm < Tramway::BaseForm
|
|
160
|
-
properties :email, :about_me
|
|
196
|
+
properties :email, :about_me, :user_type, :score
|
|
161
197
|
|
|
162
198
|
fields email: :email,
|
|
163
199
|
name: :text,
|
|
164
200
|
about_me: {
|
|
165
201
|
type: :text_area,
|
|
166
202
|
rows: 5
|
|
203
|
+
},
|
|
204
|
+
user_type: {
|
|
205
|
+
type: :select,
|
|
206
|
+
collection: ['regular', 'user']
|
|
207
|
+
},
|
|
208
|
+
score: {
|
|
209
|
+
type: :number,
|
|
210
|
+
value: -> (object) { Score.find_by(user_id: object.id).value }
|
|
167
211
|
}
|
|
212
|
+
|
|
213
|
+
def score=(value)
|
|
214
|
+
Score.find_by(user_id: object.id).update(value:)
|
|
215
|
+
end
|
|
168
216
|
end
|
|
169
217
|
```
|
|
170
218
|
|
|
@@ -537,6 +585,25 @@ class ParticipantsController < ApplicationController
|
|
|
537
585
|
end
|
|
538
586
|
```
|
|
539
587
|
|
|
588
|
+
`tramway_form_for` example
|
|
589
|
+
|
|
590
|
+
```ruby
|
|
591
|
+
= tramway_form_for @user do |f|
|
|
592
|
+
= f.email_field :email
|
|
593
|
+
= f.password_field :password
|
|
594
|
+
= f.select :role, [["Admin", "admin"], ["Manager", "manager"]], include_blank: "Select role"
|
|
595
|
+
= f.submit 'Save'
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
`tramway_form_for` supports `horizontal: true` for horizontal form layout.
|
|
599
|
+
|
|
600
|
+
```ruby
|
|
601
|
+
= tramway_form_for @user, horizontal: true do |f|
|
|
602
|
+
= f.email_field :email
|
|
603
|
+
= f.password_field :password
|
|
604
|
+
= f.submit 'Save'
|
|
605
|
+
```
|
|
606
|
+
|
|
540
607
|
|
|
541
608
|
---
|
|
542
609
|
|
data/lib/tramway/config.rb
CHANGED
data/lib/tramway/engine.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Tramway
|
|
|
13
13
|
load_form_helper
|
|
14
14
|
load_routes_helper
|
|
15
15
|
load_chats_broadcast
|
|
16
|
-
|
|
16
|
+
load_searchable
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
initializer 'tramway.assets.precompile' do |app|
|
|
@@ -78,6 +78,14 @@ module Tramway
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
def load_searchable
|
|
82
|
+
ActiveSupport.on_load(:active_record) do |loaded_class|
|
|
83
|
+
require 'tramway/searchable'
|
|
84
|
+
|
|
85
|
+
loaded_class.include Tramway::Searchable
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
81
89
|
def configure_pagination
|
|
82
90
|
ActiveSupport.on_load(:action_controller) do
|
|
83
91
|
# Detecting tramway views path
|
|
@@ -9,10 +9,12 @@ module Tramway
|
|
|
9
9
|
FORM_SIZES = %i[small medium large].freeze
|
|
10
10
|
|
|
11
11
|
def tramway_form_for(object, *, size: :medium, **options, &)
|
|
12
|
+
form_object_class = object.is_a?(Tramway::BaseForm) ? object.class : nil
|
|
13
|
+
|
|
12
14
|
form_for(
|
|
13
15
|
object,
|
|
14
16
|
*,
|
|
15
|
-
**options.merge(builder: Tramway::Form::Builder, size: normalize_form_size(size)),
|
|
17
|
+
**options.merge(builder: Tramway::Form::Builder, size: normalize_form_size(size), form_object_class:),
|
|
16
18
|
&
|
|
17
19
|
)
|
|
18
20
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
# Searchable module provides a class method `tramway_search` for ActiveRecord models to perform full-text search
|
|
5
|
+
# across all string and text columns using PostgreSQL's full-text search capabilities.
|
|
6
|
+
module Searchable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def tramway_search(query)
|
|
11
|
+
tokens = search_tokens(query)
|
|
12
|
+
columns_to_search = searchable_column_names
|
|
13
|
+
|
|
14
|
+
return all if tokens.empty? || columns_to_search.empty?
|
|
15
|
+
|
|
16
|
+
where(
|
|
17
|
+
Arel.sql("to_tsvector('simple', #{tsvector_expression(columns_to_search)}) @@ to_tsquery('simple', ?)"),
|
|
18
|
+
tsquery_expression(tokens)
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def search_tokens(query)
|
|
25
|
+
query.to_s.scan(/[[:alnum:]]+/)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def searchable_column_names
|
|
29
|
+
columns.select { |column| column.type.in?(%i[string text]) }.map(&:name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def tsvector_expression(column_names)
|
|
33
|
+
column_names.map do |column_name|
|
|
34
|
+
"coalesce(#{connection.quote_column_name(column_name)}, '')"
|
|
35
|
+
end.join(" || ' ' || ")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def tsquery_expression(tokens)
|
|
39
|
+
tokens.map { |token| "#{token}:*" }.join(' & ')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/tramway/utils/field.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Tramway
|
|
|
7
7
|
def tramway_field(field_data, attribute, **options, &)
|
|
8
8
|
input_type = field_type(field_data)
|
|
9
9
|
input_name = field_name input_type
|
|
10
|
-
input_options = field_options(field_data).merge(options)
|
|
10
|
+
input_options = field_options(field_data).merge(options.compact)
|
|
11
11
|
|
|
12
12
|
case input_type.to_sym
|
|
13
13
|
when :select, :multiselect
|
|
@@ -42,7 +42,7 @@ module Tramway
|
|
|
42
42
|
|
|
43
43
|
def field_options(field_data)
|
|
44
44
|
if field_data.is_a?(Hash)
|
|
45
|
-
value = field_data[:value]&.call
|
|
45
|
+
value = field_data[:value]&.call(object)
|
|
46
46
|
|
|
47
47
|
field_data.merge(value:).except(:type)
|
|
48
48
|
else
|
data/lib/tramway/version.rb
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tramway
|
|
4
|
+
# Warnings module provides methods to log warnings for different scenarios
|
|
5
|
+
module Warnings
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def search_fallback(model_class)
|
|
9
|
+
Rails.logger.warn(
|
|
10
|
+
"Tramway search: `#{model_class}.search` is not defined. " \
|
|
11
|
+
"Falling back to `#{model_class}.tramway_search`. " \
|
|
12
|
+
'This is a generic fallback and not tailored to your data structure, ' \
|
|
13
|
+
'so it is not intended for long-term use and may be slow or not scalable.'
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tramway
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: '3.0'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kalashnikovisme
|
|
@@ -161,6 +161,8 @@ files:
|
|
|
161
161
|
- app/components/tramway/chats/messages/table_component.html.haml
|
|
162
162
|
- app/components/tramway/chats/messages/table_component.rb
|
|
163
163
|
- app/components/tramway/colors_methods.rb
|
|
164
|
+
- app/components/tramway/comments/show_path_component.html.haml
|
|
165
|
+
- app/components/tramway/comments/show_path_component.rb
|
|
164
166
|
- app/components/tramway/containers/main_component.html.haml
|
|
165
167
|
- app/components/tramway/containers/main_component.rb
|
|
166
168
|
- app/components/tramway/containers/narrow_component.html.haml
|
|
@@ -200,6 +202,8 @@ files:
|
|
|
200
202
|
- app/components/tramway/form/text_area_component.rb
|
|
201
203
|
- app/components/tramway/form/text_field_component.html.haml
|
|
202
204
|
- app/components/tramway/form/text_field_component.rb
|
|
205
|
+
- app/components/tramway/form/time_field_component.html.haml
|
|
206
|
+
- app/components/tramway/form/time_field_component.rb
|
|
203
207
|
- app/components/tramway/native_text_component.html.haml
|
|
204
208
|
- app/components/tramway/native_text_component.rb
|
|
205
209
|
- app/components/tramway/nav/item/button_component.html.haml
|
|
@@ -286,10 +290,12 @@ files:
|
|
|
286
290
|
- lib/tramway/helpers/routes_helper.rb
|
|
287
291
|
- lib/tramway/helpers/views_helper.rb
|
|
288
292
|
- lib/tramway/navbar.rb
|
|
293
|
+
- lib/tramway/searchable.rb
|
|
289
294
|
- lib/tramway/utils/field.rb
|
|
290
295
|
- lib/tramway/utils/render.rb
|
|
291
296
|
- lib/tramway/version.rb
|
|
292
297
|
- lib/tramway/views/form_builder.rb
|
|
298
|
+
- lib/tramway/warnings.rb
|
|
293
299
|
- lib/types.rb
|
|
294
300
|
homepage: https://github.com/purple-magic/tramway
|
|
295
301
|
licenses:
|