tramway 3.1.2.6 → 3.1.2.7
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 +83 -1
- data/app/components/tailwind_component.rb +5 -0
- data/app/components/tramway/form/builder.rb +5 -0
- data/app/components/tramway/form/checkbox_component.html.haml +32 -29
- data/app/components/tramway/form/date_field_component.html.haml +2 -0
- data/app/components/tramway/form/datetime_field_component.html.haml +2 -0
- data/app/components/tramway/form/file_field_component.html.haml +2 -0
- data/app/components/tramway/form/number_field_component.html.haml +2 -0
- data/app/components/tramway/form/rich_text_area_component.html.haml +2 -0
- data/app/components/tramway/form/select_component.html.haml +2 -0
- data/app/components/tramway/form/text_area_component.html.haml +2 -0
- data/app/components/tramway/form/text_field_component.html.haml +2 -0
- data/app/components/tramway/form/time_field_component.html.haml +2 -0
- data/app/components/tramway/form/tramway_select_component.html.haml +2 -0
- data/app/controllers/tramway/entities_controller.rb +1 -29
- data/app/views/tramway/entities/show.html.haml +4 -3
- data/docs/AGENTS.md +4 -1
- data/lib/tramway/base_decorator.rb +2 -0
- data/lib/tramway/decorators/show_associations.rb +101 -0
- data/lib/tramway/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1bc6a1e219c499e701dd4ec09e3e04d722ba480744c25db62f4b8f5b75f3c365
|
|
4
|
+
data.tar.gz: 5fd9987b47419d936e0bd50dbf727147c149c33948188d53589cc3666482829b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c4fb50d8f6579be7bb697af7a93b3e1b69199724e6ca23b1a54e03809bc8de896a43d2faf22dcfd0bc3f1755648689c888659c0ffcb19f90407a2036a2c0d44
|
|
7
|
+
data.tar.gz: cc2131f3a6f9302ab31db63ca04fce6339e770c9694a97d147e340e8294002ed1707bd5ed5c511b2e1293f369f23139bd57e6236d467fcc9a7f84b8bc0e00e68
|
data/README.md
CHANGED
|
@@ -535,6 +535,86 @@ class UserDecorator < Tramway::BaseDecorator
|
|
|
535
535
|
end
|
|
536
536
|
```
|
|
537
537
|
|
|
538
|
+
#### Show associations
|
|
539
|
+
|
|
540
|
+
Entity show pages can render associated records by returning association names from
|
|
541
|
+
the decorator's `show_associations` method. Each name must be an actual
|
|
542
|
+
Active Record association on the decorated model; Tramway raises a clear error
|
|
543
|
+
when the association is missing.
|
|
544
|
+
|
|
545
|
+
*app/models/post.rb*
|
|
546
|
+
```ruby
|
|
547
|
+
class Post < ApplicationRecord
|
|
548
|
+
has_many :comments
|
|
549
|
+
end
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
*app/decorators/post_decorator.rb*
|
|
553
|
+
```ruby
|
|
554
|
+
class PostDecorator < Tramway::BaseDecorator
|
|
555
|
+
association :comments
|
|
556
|
+
|
|
557
|
+
def show_associations
|
|
558
|
+
%i[comments]
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Tramway renders a `New` button for a shown association when it can determine a
|
|
564
|
+
create path for that association. Define the associated model as a Tramway entity
|
|
565
|
+
with a `:create` page to let Tramway build that path automatically:
|
|
566
|
+
|
|
567
|
+
*config/initializers/tramway.rb*
|
|
568
|
+
```ruby
|
|
569
|
+
Tramway.configure do |config|
|
|
570
|
+
config.entities = [
|
|
571
|
+
{
|
|
572
|
+
name: :post,
|
|
573
|
+
pages: [
|
|
574
|
+
{ action: :show }
|
|
575
|
+
]
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: :comment,
|
|
579
|
+
pages: [
|
|
580
|
+
{ action: :create }
|
|
581
|
+
]
|
|
582
|
+
}
|
|
583
|
+
]
|
|
584
|
+
end
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
When `new_record_path` is omitted, Tramway looks for a configured entity using
|
|
588
|
+
the association reflection's class name in underscore format. This means aliases
|
|
589
|
+
such as `has_many :responses, class_name: 'Comment'` use the `comment` entity,
|
|
590
|
+
not a `response` entity.
|
|
591
|
+
|
|
592
|
+
Generated association create paths include a `redirect` query parameter pointing
|
|
593
|
+
to the current record's configured Tramway entity show path, for example:
|
|
594
|
+
`/comments/new?redirect=%2Fposts%2F1`.
|
|
595
|
+
|
|
596
|
+
You can override the path explicitly when the associated record should be created
|
|
597
|
+
through a custom route:
|
|
598
|
+
|
|
599
|
+
```ruby
|
|
600
|
+
class PostDecorator < Tramway::BaseDecorator
|
|
601
|
+
association :comments
|
|
602
|
+
|
|
603
|
+
def show_associations
|
|
604
|
+
[
|
|
605
|
+
[
|
|
606
|
+
:comments,
|
|
607
|
+
{ new_record_path: Rails.application.routes.url_helpers.new_post_comment_path(object) }
|
|
608
|
+
]
|
|
609
|
+
]
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Tramway still appends the `redirect` query parameter to custom paths. Associations
|
|
615
|
+
without a create route and without `new_record_path` still render their records,
|
|
616
|
+
but no creation button is displayed.
|
|
617
|
+
|
|
538
618
|
#### Decorate nil
|
|
539
619
|
|
|
540
620
|
Tramway Decorator does not decorate nil objects
|
|
@@ -1253,10 +1333,12 @@ will render [this](https://play.tailwindcss.com/xho3LfjKkK)
|
|
|
1253
1333
|
Use `size:` to control the input sizing (`:small`, `:medium`, or `:large`). The default is `:medium`, and supported inputs
|
|
1254
1334
|
rendered within the form will use the same size value.
|
|
1255
1335
|
Date and datetime fields open the browser's native picker when clicking anywhere in the input, not only the picker icon.
|
|
1336
|
+
Use `hint:` on any field to render muted helper text below the input. The hint option is handled by Tramway and is not
|
|
1337
|
+
forwarded as an HTML attribute.
|
|
1256
1338
|
|
|
1257
1339
|
```erb
|
|
1258
1340
|
<%= tramway_form_for @user, size: :large do |f| %>
|
|
1259
|
-
<%= f.text_field :text %>
|
|
1341
|
+
<%= f.text_field :text, hint: 'This name is visible to teammates.' %>
|
|
1260
1342
|
<%= f.submit 'Create User' %>
|
|
1261
1343
|
<% end %>
|
|
1262
1344
|
```
|
|
@@ -10,6 +10,7 @@ class TailwindComponent < Tramway::BaseComponent
|
|
|
10
10
|
option :options
|
|
11
11
|
option :label
|
|
12
12
|
option :for
|
|
13
|
+
option :hint, optional: true, default: -> {}
|
|
13
14
|
option :size, default: -> { :medium }
|
|
14
15
|
|
|
15
16
|
SIZE_CLASSES = {
|
|
@@ -83,6 +84,10 @@ class TailwindComponent < Tramway::BaseComponent
|
|
|
83
84
|
'peer-disabled:opacity-70'
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
def form_hint_classes
|
|
88
|
+
'mt-1 text-sm text-zinc-400'
|
|
89
|
+
end
|
|
90
|
+
|
|
86
91
|
def default_container_classes
|
|
87
92
|
return if options[:horizontal]
|
|
88
93
|
|
|
@@ -210,11 +210,16 @@ module Tramway
|
|
|
210
210
|
attribute:,
|
|
211
211
|
label: label_build(attribute, options),
|
|
212
212
|
for: options[:id].presence || for_id(attribute),
|
|
213
|
+
hint: hint_option(options),
|
|
213
214
|
options: options,
|
|
214
215
|
size: form_size
|
|
215
216
|
}
|
|
216
217
|
end
|
|
217
218
|
|
|
219
|
+
def hint_option(options)
|
|
220
|
+
options.delete(:hint) || options.delete('hint')
|
|
221
|
+
end
|
|
222
|
+
|
|
218
223
|
def label_build(attribute, options)
|
|
219
224
|
label_option = options[:label]
|
|
220
225
|
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
%
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
= @
|
|
1
|
+
%div{ class: default_container_classes, data: { controller: 'ui--checkbox' } }
|
|
2
|
+
.flex.items-center.gap-2
|
|
3
|
+
- checkbox_checked = checked?
|
|
4
|
+
= @input.call @attribute, **hidden_checkbox_options
|
|
5
|
+
%button{ type: 'button',
|
|
6
|
+
role: 'checkbox',
|
|
7
|
+
'aria-checked' => checkbox_checked.to_s,
|
|
8
|
+
'data-action' => 'click->ui--checkbox#toggle',
|
|
9
|
+
'data-state' => checkbox_checked ? 'checked' : 'unchecked',
|
|
10
|
+
'data-ui--checkbox-target' => 'button',
|
|
11
|
+
value: 'on',
|
|
12
|
+
class: checkbox_button_classes,
|
|
13
|
+
style: checkbox_button_style,
|
|
14
|
+
for: @for,
|
|
15
|
+
id: @for }
|
|
16
|
+
%span{ class: checkbox_indicator_classes, style: 'pointer-events: none', data: { 'ui--checkbox-target' => 'indicator' } }
|
|
17
|
+
%svg{ xmlns: 'http://www.w3.org/2000/svg',
|
|
18
|
+
width: '24',
|
|
19
|
+
height: '24',
|
|
20
|
+
viewBox: '0 0 24 24',
|
|
21
|
+
fill: 'none',
|
|
22
|
+
stroke: 'currentColor',
|
|
23
|
+
'stroke-width' => '2',
|
|
24
|
+
'stroke-linecap' => 'round',
|
|
25
|
+
'stroke-linejoin' => 'round',
|
|
26
|
+
class: size_class(:checkbox_indicator) }
|
|
27
|
+
%polyline{ points: '20 6 9 17 4 12' }
|
|
28
|
+
- if @label
|
|
29
|
+
= component('tramway/form/label', for: @for, options: { class: label_classes, style: label_style, data: { action: 'click->ui--checkbox#toggle' } }) do
|
|
30
|
+
= @label
|
|
31
|
+
- if @hint.present?
|
|
32
|
+
%p{ class: form_hint_classes }= @hint
|
|
@@ -11,3 +11,5 @@
|
|
|
11
11
|
= component 'tramway/form/tramway_select/caret', size:, direction: :down
|
|
12
12
|
.caret-up.hidden{ data: { "tramway-select-target" => "caretUp" } }
|
|
13
13
|
= component 'tramway/form/tramway_select/caret', size:, direction: :up
|
|
14
|
+
- if @hint.present?
|
|
15
|
+
%p{ class: form_hint_classes }= @hint
|
|
@@ -91,35 +91,7 @@ module Tramway
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def set_associations
|
|
94
|
-
@associations = @record.
|
|
95
|
-
options ||= {}
|
|
96
|
-
association_type = @record.object.class.reflect_on_association(association).macro
|
|
97
|
-
|
|
98
|
-
{
|
|
99
|
-
name: association,
|
|
100
|
-
association_type:,
|
|
101
|
-
**send("#{association_type}_associations", association, options)
|
|
102
|
-
}
|
|
103
|
-
end.compact
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# rubocop:disable Naming/PredicatePrefix
|
|
107
|
-
def has_many_associations(association, options)
|
|
108
|
-
records = Kaminari.paginate_array(@record.public_send(association.name)).page(params[:page])
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
decorator: records.first&.class,
|
|
112
|
-
records:,
|
|
113
|
-
model_class: records.first&.object&.class,
|
|
114
|
-
new_record_path: options[:new_record_path]
|
|
115
|
-
}
|
|
116
|
-
end
|
|
117
|
-
# rubocop:enable Naming/PredicatePrefix
|
|
118
|
-
|
|
119
|
-
def belongs_to_associations(association, _options)
|
|
120
|
-
record = @record.public_send(association.name)
|
|
121
|
-
|
|
122
|
-
{ decorator: record.class, record:, model_class: record.class }
|
|
94
|
+
@associations = @record.send(:__show_associations, params[:page])
|
|
123
95
|
end
|
|
124
96
|
|
|
125
97
|
def search(entities)
|
|
@@ -52,9 +52,10 @@
|
|
|
52
52
|
custom_path_method: @entity.index_helper_method,
|
|
53
53
|
custom_path_arguments: [@record.id]
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
- if association[:new_record_path].present?
|
|
56
|
+
= tramway_button text: t('tramway.actions.new'),
|
|
57
|
+
path: association[:new_record_path],
|
|
58
|
+
type: :will
|
|
58
59
|
|
|
59
60
|
- if association[:records].any?
|
|
60
61
|
= tramway_table class: 'mt-4' do
|
data/docs/AGENTS.md
CHANGED
|
@@ -712,12 +712,15 @@ end
|
|
|
712
712
|
|
|
713
713
|
```ruby
|
|
714
714
|
= tramway_form_for @user do |f|
|
|
715
|
-
= f.email_field :email
|
|
715
|
+
= f.email_field :email, hint: 'Use the address where you receive account notifications.'
|
|
716
716
|
= f.password_field :password
|
|
717
717
|
= f.select :role, [["Admin", "admin"], ["Manager", "manager"]], include_blank: "Select role"
|
|
718
718
|
= f.submit 'Save'
|
|
719
719
|
```
|
|
720
720
|
|
|
721
|
+
Use `hint:` on fields when short helper text should appear below the input. Tramway renders the hint separately and does
|
|
722
|
+
not forward it to the input as an HTML attribute.
|
|
723
|
+
|
|
721
724
|
Autocomplete select example:
|
|
722
725
|
|
|
723
726
|
```ruby
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'tramway/decorators/name_builder'
|
|
4
4
|
require 'tramway/decorators/association'
|
|
5
5
|
require 'tramway/decorators/collection_decorator'
|
|
6
|
+
require 'tramway/decorators/show_associations'
|
|
6
7
|
require 'tramway/helpers/decorate_helper'
|
|
7
8
|
require 'tramway/helpers/component_helper'
|
|
8
9
|
require 'tramway/helpers/views_helper'
|
|
@@ -14,6 +15,7 @@ module Tramway
|
|
|
14
15
|
#
|
|
15
16
|
class BaseDecorator
|
|
16
17
|
include Tramway::Decorators::CollectionDecorators
|
|
18
|
+
include Tramway::Decorators::ShowAssociations
|
|
17
19
|
include Tramway::Utils::Render
|
|
18
20
|
include Tramway::DuckTyping::ActiveRecordCompatibility
|
|
19
21
|
include Tramway::Helpers::DecorateHelper
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack/utils'
|
|
4
|
+
|
|
5
|
+
module Tramway
|
|
6
|
+
module Decorators
|
|
7
|
+
# Builds metadata for associations rendered on Tramway entity show pages.
|
|
8
|
+
module ShowAssociations
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def __show_associations(page)
|
|
12
|
+
show_associations.map do |(association, options)|
|
|
13
|
+
__show_association(association, options || {}, page)
|
|
14
|
+
end.compact
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def __show_association(association, options, page)
|
|
18
|
+
reflection = __reflection_for_show_association(association)
|
|
19
|
+
association_type = reflection.macro
|
|
20
|
+
association_options = __association_options(association, association_type, options, page)
|
|
21
|
+
new_record_path = __association_new_record_path(reflection, association_options.delete(:new_record_path))
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
name: association,
|
|
25
|
+
association_type:,
|
|
26
|
+
new_record_path:,
|
|
27
|
+
**association_options
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def __reflection_for_show_association(association)
|
|
32
|
+
reflection = object.class.reflect_on_association(association)
|
|
33
|
+
|
|
34
|
+
return reflection if reflection.present?
|
|
35
|
+
|
|
36
|
+
raise "You must define #{association} association in the #{object.class}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def __association_options(association, association_type, options, page)
|
|
40
|
+
send("__#{association_type}_associations", association, options, page)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def __association_new_record_path(reflection, new_record_path)
|
|
44
|
+
new_record_path = __inferred_association_new_record_path(reflection) if new_record_path.blank?
|
|
45
|
+
|
|
46
|
+
return if new_record_path.blank?
|
|
47
|
+
|
|
48
|
+
__new_record_path_with_redirect(new_record_path)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def __inferred_association_new_record_path(reflection)
|
|
52
|
+
association_entity = __entity_for_name(reflection.class_name.underscore)
|
|
53
|
+
|
|
54
|
+
return if association_entity.blank? || association_entity.page(:create).blank?
|
|
55
|
+
|
|
56
|
+
Tramway::Engine.routes.url_helpers.public_send(association_entity.new_helper_method)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def __new_record_path_with_redirect(new_record_path)
|
|
60
|
+
redirect_path = __entity_show_path
|
|
61
|
+
|
|
62
|
+
return new_record_path if redirect_path.blank?
|
|
63
|
+
|
|
64
|
+
separator = new_record_path.include?('?') ? '&' : '?'
|
|
65
|
+
|
|
66
|
+
"#{new_record_path}#{separator}#{Rack::Utils.build_query(redirect: redirect_path)}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def __entity_show_path
|
|
70
|
+
entity = __entity_for_name(object.class.name.underscore)
|
|
71
|
+
|
|
72
|
+
return if entity.blank? || entity.page(:show).blank?
|
|
73
|
+
|
|
74
|
+
Tramway::Engine.routes.url_helpers.public_send(entity.show_helper_method, object.id)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def __entity_for_name(name)
|
|
78
|
+
Tramway.config.entities.find do |config_entity|
|
|
79
|
+
config_entity.name == name
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def __has_many_associations(association, options, page)
|
|
84
|
+
records = Kaminari.paginate_array(public_send(association.name)).page(page)
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
decorator: records.first&.class,
|
|
88
|
+
records:,
|
|
89
|
+
model_class: records.first&.object&.class,
|
|
90
|
+
new_record_path: options[:new_record_path]
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def __belongs_to_associations(association, _options, _page)
|
|
95
|
+
record = public_send(association.name)
|
|
96
|
+
|
|
97
|
+
{ decorator: record.class, record:, model_class: record.class }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/tramway/version.rb
CHANGED
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: 3.1.2.
|
|
4
|
+
version: 3.1.2.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kalashnikovisme
|
|
@@ -282,6 +282,7 @@ files:
|
|
|
282
282
|
- lib/tramway/decorators/class_helper.rb
|
|
283
283
|
- lib/tramway/decorators/collection_decorator.rb
|
|
284
284
|
- lib/tramway/decorators/name_builder.rb
|
|
285
|
+
- lib/tramway/decorators/show_associations.rb
|
|
285
286
|
- lib/tramway/duck_typing.rb
|
|
286
287
|
- lib/tramway/duck_typing/active_record_compatibility.rb
|
|
287
288
|
- lib/tramway/engine.rb
|