tramway 4.1.0.1 → 4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7992bbaa872dc64b21203cb0350463136687e0bfe033a1b461c87bf15804f6b
4
- data.tar.gz: 24024ccfa23c7e0718a8dcfd708ce4fcfe232e67c352e5fa28360b017f0b323e
3
+ metadata.gz: aba7799f773e66098f92d1471963e6fe5da4079b095264c249dc4545a1a84a00
4
+ data.tar.gz: 98866e7d39aa6d9a31eb9b750949d69ccd16cd2d2f8cb2421f9eb9b96b450046
5
5
  SHA512:
6
- metadata.gz: 5a8e09bf7ab1d4c09d881f87d99cd1ab5841e92b29a17e2bab144bc009e70263af0cf3ffc71930a027dd589beb13f50c933f4ffcc9fb292e3c36e75f2015b699
7
- data.tar.gz: 0110e0d22d0f745d459021d97b929ed4915e08d292a706d7a06dd0c409cd2426c42f2fdb0e40af5ef243e1609b8c37c5ad20204c38b7d16db4f4d0f9d5599b89
6
+ metadata.gz: 27c456947b97709869bf23bab9eb3f72b4f30799766755fd53c58dad2ec070f0df73b574c16cea5983d3f560983ee9adb0194d84535553ba89d1748a90490005
7
+ data.tar.gz: b3b705af11f591965764edac7608d8af72e156d1a78d029a887e96b20b30945818df89f4ea947a0f6d945f06f90bf6db702b1a8951597c5cadb9f196c45e6227
data/README.md CHANGED
@@ -177,6 +177,30 @@ Tramway.configure do |config|
177
177
  end
178
178
  ```
179
179
 
180
+ **icon**
181
+
182
+ Set `icon` on an entity to render a Font Awesome class string before its auto-generated link in the Tramway Navbar
183
+ (the entity-based items rendered when `tramway_navbar` is called with `with_entities: true`, the default — see
184
+ [Icons on navbar items](#icons-on-navbar-items)):
185
+
186
+ *config/initializers/tramway.rb*
187
+ ```ruby
188
+ Tramway.configure do |config|
189
+ config.entities = [
190
+ {
191
+ name: :campaign,
192
+ namespace: :admin,
193
+ icon: 'fa-solid fa-bullhorn',
194
+ pages: [
195
+ { action: :index }
196
+ ]
197
+ }
198
+ ]
199
+ end
200
+ ```
201
+
202
+ Entities without an `icon` keep rendering as plain text links.
203
+
180
204
  **scope**
181
205
 
182
206
  By default, Tramway lists all records for an entity on the index page. You can narrow the records displayed by providing a
@@ -331,6 +355,18 @@ end
331
355
  With this configuration in place, visiting the show page displays a two-column table where the left column contains the
332
356
  localized attribute names and the right column renders their values.
333
357
 
358
+ Each entry in `show_attributes` can also be a Hash instead of a plain symbol, to pass options to the `tramway_cell`
359
+ rendering that attribute's value. The Hash requires an `attribute:` key (the method to display) and accepts an
360
+ `options:` key (a Hash forwarded as-is to `tramway_cell`, e.g. `truncate: false`):
361
+
362
+ ```ruby
363
+ class CampaignDecorator < Tramway::BaseDecorator
364
+ def show_attributes
365
+ [:name, { attribute: :status, options: { truncate: false } }, :starts_at]
366
+ end
367
+ end
368
+ ```
369
+
334
370
  **create page**
335
371
 
336
372
  To render a create page for an entity, declare a `:create` action inside the `pages` array in
@@ -430,6 +466,62 @@ Tramway.configure do |config|
430
466
  end
431
467
  ```
432
468
 
469
+ **custom page**
470
+
471
+ Besides the built-in `index`, `show`, `create`, `update`, and `destroy` pages, an entity can declare **Custom Pages**:
472
+ your own controller action and view, loaded through the same routing, layout, and navbar as the rest of the entity's
473
+ pages. Use a Custom Page for anything that doesn't fit the built-in CRUD actions — a report, a bulk tool, an
474
+ "impersonate" button's target page, and so on.
475
+
476
+ Declare it by adding a page whose `action` is not one of the built-in ones:
477
+
478
+ ```ruby
479
+ Tramway.configure do |config|
480
+ config.entities = [
481
+ {
482
+ name: :user,
483
+ namespace: :admin,
484
+ pages: [
485
+ { action: :index },
486
+ {
487
+ action: :impersonate,
488
+ member: true, # nests the route under /:id (default: false, an entity-level route)
489
+ via: :post, # HTTP method(s) the route accepts (default: :get); also accepts an array, e.g. %i[get post]
490
+ params: [:redirect_to] # extra required path segments, e.g. /admin/users/:id/impersonate/:redirect_to
491
+ }
492
+ ]
493
+ }
494
+ ]
495
+ end
496
+ ```
497
+
498
+ Then implement the action yourself, by Rails naming convention, in the controller Tramway would otherwise generate
499
+ for that namespace/entity (here, `Admin::UsersController`), inheriting from `Tramway::EntitiesController` so it gets
500
+ the same layout, navbar, and `entity`/`model_class` lookups as the built-in pages:
501
+
502
+ ```ruby
503
+ # app/controllers/admin/users_controller.rb
504
+ module Admin
505
+ class UsersController < Tramway::EntitiesController
506
+ def impersonate
507
+ @user = model_class.find(params[:id])
508
+ # ...
509
+ end
510
+ end
511
+ end
512
+ ```
513
+
514
+ And add its view at the conventional Rails path for that controller/action:
515
+
516
+ ```haml
517
+ -# app/views/admin/users/impersonate.html.haml
518
+ = tramway_container do
519
+ %p Impersonating #{@user.email}
520
+ ```
521
+
522
+ See [docs/users/tramway_custom_pages.md](docs/users/tramway_custom_pages.md) for how the routing/controller/view
523
+ resolution works under the hood.
524
+
433
525
  **route_helper**
434
526
 
435
527
  To get routes Tramway generated just Tramway::Engine.
@@ -990,7 +1082,9 @@ will render [this](https://play.tailwindcss.com/UZPTCudFw5)
990
1082
 
991
1083
  #### Icons on navbar items
992
1084
 
993
- `nav.item` accepts an optional `icon:` option with a Font Awesome class string, rendered before the item's label:
1085
+ `nav.item` accepts an optional `icon:` option with a Font Awesome class string, rendered before the item's label.
1086
+ This also applies to the auto-generated entity links described in [Define entities with options](#define-entities-with-options) —
1087
+ set `icon` on an entity in `config/initializers/tramway.rb` to add an icon to its link without writing a custom `nav.item` call.
994
1088
 
995
1089
  ```ruby
996
1090
  nav.left do
@@ -1319,6 +1413,15 @@ interactivity.
1319
1413
  Cell content that does not fit its column is truncated with an ellipsis. This is a pure CSS behavior (`truncate` +
1320
1414
  `min-w-0` on each cell) — no JavaScript is involved, and no configuration is needed to enable it.
1321
1415
 
1416
+ Pass `truncate: false` to `tramway_cell` to opt a specific cell out of this behavior and let its content wrap/overflow
1417
+ normally:
1418
+
1419
+ ```erb
1420
+ <%= tramway_cell truncate: false do %>
1421
+ <%= user.bio %>
1422
+ <% end %>
1423
+ ```
1424
+
1322
1425
  On narrow screens all columns always render, and the table scrolls horizontally within itself (the page never
1323
1426
  stretches past the viewport width) so every column stays reachable. See
1324
1427
  [docs/users/tramway_table.md](docs/users/tramway_table.md) for the full details of the mobile scroll behavior.
@@ -78,14 +78,16 @@ module Tramway
78
78
 
79
79
  private
80
80
 
81
+ # Selections round-trip as a comma-joined string; normalize back to an array here so the
82
+ # stimulus controller's JSON.parse(data-value) doesn't crash on a saved value.
83
+ def value = Array(super).flat_map { |item| item.is_a?(String) ? item.split(',') : item }.map(&:to_s)
84
+
81
85
  def tramway_select_size_class
82
86
  classes = size_class(:tramway_select_input)
83
87
  multiple ? classes.gsub(/\bh-/, 'min-h-') : classes
84
88
  end
85
89
 
86
- def action
87
- 'click->tramway-select#toggleDropdown'
88
- end
90
+ def action = 'click->tramway-select#toggleDropdown'
89
91
 
90
92
  def items
91
93
  collection
@@ -5,17 +5,17 @@ module Tramway
5
5
  # Component for rendering a cell in a table
6
6
  class CellComponent < Tramway::BaseComponent
7
7
  SIZE_CLASSES = {
8
- small: 'div-table-cell truncate min-w-0 bg-transparent px-4 py-2 text-sm font-medium text-zinc-100',
9
- medium: 'div-table-cell truncate min-w-0 bg-transparent px-6 py-4 text-base font-medium ' \
10
- 'text-zinc-100',
11
- large: 'div-table-cell truncate min-w-0 bg-transparent px-6 py-6 text-lg font-medium ' \
12
- 'text-zinc-100'
8
+ small: 'div-table-cell min-w-0 bg-transparent px-4 py-2 text-sm font-medium text-zinc-100',
9
+ medium: 'div-table-cell min-w-0 bg-transparent px-6 py-4 text-base font-medium text-zinc-100',
10
+ large: 'div-table-cell min-w-0 bg-transparent px-6 py-6 text-lg font-medium text-zinc-100'
13
11
  }.freeze
14
12
 
15
13
  option :options, optional: true, default: -> { {} }
14
+ option :truncate, optional: true, default: -> { true }
16
15
 
17
16
  def cell_classes
18
- SIZE_CLASSES.fetch(tramway_table_size) { SIZE_CLASSES[:medium] }
17
+ base_classes = SIZE_CLASSES.fetch(tramway_table_size) { SIZE_CLASSES[:medium] }
18
+ truncate ? "#{base_classes} truncate" : base_classes
19
19
  end
20
20
  end
21
21
  end
@@ -35,11 +35,13 @@
35
35
  = association[:record].title
36
36
 
37
37
  - @record.show_attributes.each do |attribute|
38
+ - attribute_name = @record.show_attribute_name(attribute)
39
+ - attribute_options = @record.show_attribute_options(attribute)
38
40
  = tramway_row do
39
41
  = tramway_cell do
40
- = @model_class.human_attribute_name(attribute)
41
- = tramway_cell do
42
- = @record.public_send(attribute)
42
+ = @model_class.human_attribute_name(attribute_name)
43
+ = tramway_cell(**attribute_options) do
44
+ = @record.public_send(attribute_name)
43
45
 
44
46
  - @associations.select { _1[:association_type] == :has_many }.each do |association|
45
47
  .flex.flex-row.justify-between.mt-8.text-white
data/config/routes.rb CHANGED
@@ -13,9 +13,10 @@ Tramway::Engine.routes.draw do
13
13
  end => segments
14
14
 
15
15
  resource_name = segments.pop
16
+ controller_path = (segments + [resource_name.pluralize]).join('/')
16
17
 
17
18
  define_resource = proc do
18
- entity.pages.reduce([]) do |acc, page|
19
+ entity.built_in_pages.reduce([]) do |acc, page|
19
20
  case page.action
20
21
  when 'index'
21
22
  acc << :index
@@ -27,15 +28,24 @@ Tramway::Engine.routes.draw do
27
28
  acc + %i[edit update]
28
29
  when 'destroy'
29
30
  acc << :destroy
30
- else
31
- acc
32
31
  end
33
32
  end => actions
34
33
 
35
34
  resources resource_name.pluralize.to_sym,
36
35
  only: actions.map(&:to_sym),
37
36
  controller: '/tramway/entities',
38
- defaults: { entity: }
37
+ defaults: { entity: } do
38
+ entity.custom_pages.each do |page|
39
+ path = ([page.action] + page.params.map { |param| ":#{param}" }).join('/')
40
+ to = "/#{controller_path}##{page.action}"
41
+
42
+ if page.member
43
+ member { match path, to:, via: page.via, defaults: { entity: } }
44
+ else
45
+ collection { match path, to:, via: page.via, defaults: { entity: } }
46
+ end
47
+ end
48
+ end
39
49
  end
40
50
 
41
51
  if segments.empty?
@@ -92,6 +92,20 @@ module Tramway
92
92
  []
93
93
  end
94
94
 
95
+ def show_attribute_name(attribute)
96
+ return attribute unless attribute.is_a?(Hash)
97
+
98
+ attribute.fetch(:attribute) do
99
+ raise ArgumentError, 'Each show_attributes Hash entry must include an :attribute key'
100
+ end
101
+ end
102
+
103
+ def show_attribute_options(attribute)
104
+ return {} unless attribute.is_a?(Hash)
105
+
106
+ attribute.fetch(:options, {})
107
+ end
108
+
95
109
  def show_associations
96
110
  []
97
111
  end
@@ -11,6 +11,9 @@ module Tramway
11
11
  attribute? :search, Types::Bool
12
12
  attribute? :includes, Types::Array.default([].freeze)
13
13
  attribute? :filters, Types::Array.default([].freeze)
14
+ attribute? :member, Types::Bool.default(false)
15
+ attribute? :via, (Types::Coercible::Symbol | Types::Array.of(Types::Coercible::Symbol)).default(:get)
16
+ attribute? :params, Types::Array.of(Types::Coercible::Symbol).default([].freeze)
14
17
  end
15
18
  end
16
19
  end
@@ -11,11 +11,16 @@ module Tramway
11
11
  attribute? :pages, Types::Array.of(Tramway::Configs::Entities::Page).default([].freeze)
12
12
  attribute? :route, Tramway::Configs::Entities::Route
13
13
  attribute? :namespace, Types::Coercible::String
14
+ attribute? :icon, Types::Coercible::String
14
15
 
15
16
  # Route Struct contains implemented in Tramway CRUD and helpful routes for the entity
16
17
  ACTIONS = %i[index show new create edit update destroy].freeze
17
18
  RouteStruct = Struct.new(*ACTIONS)
18
19
 
20
+ # Page#action values handled by Tramway's built-in CRUD controller and routing.
21
+ # Any other page is a Custom Page, routed to a host app controller instead.
22
+ BUILT_IN_PAGE_ACTIONS = %w[index show create update destroy].freeze
23
+
19
24
  # HumanName Struct contains human names forms for the entity
20
25
  HumanNameStruct = Struct.new(:single, :plural)
21
26
 
@@ -39,6 +44,14 @@ module Tramway
39
44
  pages.find { |page| page.action == name.to_s }
40
45
  end
41
46
 
47
+ def built_in_pages
48
+ pages.select { |page| BUILT_IN_PAGE_ACTIONS.include?(page.action) }
49
+ end
50
+
51
+ def custom_pages
52
+ pages.reject { |page| BUILT_IN_PAGE_ACTIONS.include?(page.action) }
53
+ end
54
+
42
55
  def show_helper_method
43
56
  build_helper_method(name, route:, namespace:, plural: false)
44
57
  end
@@ -50,8 +50,8 @@ module Tramway
50
50
  &
51
51
  end
52
52
 
53
- def tramway_cell(**options, &)
54
- component 'tramway/table/cell', options:, &
53
+ def tramway_cell(truncate: true, **options, &)
54
+ component 'tramway/table/cell', truncate:, options:, &
55
55
  end
56
56
 
57
57
  def tramway_button(path: nil, text: nil, method: :get, form_options: {}, **options, &)
@@ -59,7 +59,8 @@ module Tramway
59
59
  entities.each do |entity|
60
60
  next if entity.routes.index.blank?
61
61
 
62
- item entity.human_name.plural, Tramway::Engine.routes.url_helpers.public_send(entity.routes.index)
62
+ item entity.human_name.plural, Tramway::Engine.routes.url_helpers.public_send(entity.routes.index),
63
+ icon: entity.icon
63
64
  end
64
65
 
65
66
  reset_filling
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tramway
4
- VERSION = '4.1.0.1'
4
+ VERSION = '4.1.2'
5
5
  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.1.0.1
4
+ version: 4.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - kalashnikovisme