tramway 4.1.0.1 → 4.1.1

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: 157c85b21871fba844d2da67a40213a63b7613a9110e5fd8e72b430eea52c1cb
4
+ data.tar.gz: 19448d06038afe21efe938b9abe4db4eca02bb37c79b63a6d3dabc303fbff362
5
5
  SHA512:
6
- metadata.gz: 5a8e09bf7ab1d4c09d881f87d99cd1ab5841e92b29a17e2bab144bc009e70263af0cf3ffc71930a027dd589beb13f50c933f4ffcc9fb292e3c36e75f2015b699
7
- data.tar.gz: 0110e0d22d0f745d459021d97b929ed4915e08d292a706d7a06dd0c409cd2426c42f2fdb0e40af5ef243e1609b8c37c5ad20204c38b7d16db4f4d0f9d5599b89
6
+ metadata.gz: d0c54e34d0017715e2b4d0b0f351abebf0f02a3029cc4762bf40c07abd0198e8945b7b1bf3e474051263afddb9a65e518fa8febdae127b323e755c67c814049f
7
+ data.tar.gz: 8acef7a5b516284dfa3d44901cf059f8c9632aedc41bf2bbe1a93f8c7b840bb6988f424449e9fde2f53558e0f78a5dba2cc02b61d478669bdb05318d33d6102e
data/README.md CHANGED
@@ -331,6 +331,18 @@ end
331
331
  With this configuration in place, visiting the show page displays a two-column table where the left column contains the
332
332
  localized attribute names and the right column renders their values.
333
333
 
334
+ Each entry in `show_attributes` can also be a Hash instead of a plain symbol, to pass options to the `tramway_cell`
335
+ rendering that attribute's value. The Hash requires an `attribute:` key (the method to display) and accepts an
336
+ `options:` key (a Hash forwarded as-is to `tramway_cell`, e.g. `truncate: false`):
337
+
338
+ ```ruby
339
+ class CampaignDecorator < Tramway::BaseDecorator
340
+ def show_attributes
341
+ [:name, { attribute: :status, options: { truncate: false } }, :starts_at]
342
+ end
343
+ end
344
+ ```
345
+
334
346
  **create page**
335
347
 
336
348
  To render a create page for an entity, declare a `:create` action inside the `pages` array in
@@ -430,6 +442,62 @@ Tramway.configure do |config|
430
442
  end
431
443
  ```
432
444
 
445
+ **custom page**
446
+
447
+ Besides the built-in `index`, `show`, `create`, `update`, and `destroy` pages, an entity can declare **Custom Pages**:
448
+ your own controller action and view, loaded through the same routing, layout, and navbar as the rest of the entity's
449
+ pages. Use a Custom Page for anything that doesn't fit the built-in CRUD actions — a report, a bulk tool, an
450
+ "impersonate" button's target page, and so on.
451
+
452
+ Declare it by adding a page whose `action` is not one of the built-in ones:
453
+
454
+ ```ruby
455
+ Tramway.configure do |config|
456
+ config.entities = [
457
+ {
458
+ name: :user,
459
+ namespace: :admin,
460
+ pages: [
461
+ { action: :index },
462
+ {
463
+ action: :impersonate,
464
+ member: true, # nests the route under /:id (default: false, an entity-level route)
465
+ via: :post, # HTTP method(s) the route accepts (default: :get); also accepts an array, e.g. %i[get post]
466
+ params: [:redirect_to] # extra required path segments, e.g. /admin/users/:id/impersonate/:redirect_to
467
+ }
468
+ ]
469
+ }
470
+ ]
471
+ end
472
+ ```
473
+
474
+ Then implement the action yourself, by Rails naming convention, in the controller Tramway would otherwise generate
475
+ for that namespace/entity (here, `Admin::UsersController`), inheriting from `Tramway::EntitiesController` so it gets
476
+ the same layout, navbar, and `entity`/`model_class` lookups as the built-in pages:
477
+
478
+ ```ruby
479
+ # app/controllers/admin/users_controller.rb
480
+ module Admin
481
+ class UsersController < Tramway::EntitiesController
482
+ def impersonate
483
+ @user = model_class.find(params[:id])
484
+ # ...
485
+ end
486
+ end
487
+ end
488
+ ```
489
+
490
+ And add its view at the conventional Rails path for that controller/action:
491
+
492
+ ```haml
493
+ -# app/views/admin/users/impersonate.html.haml
494
+ = tramway_container do
495
+ %p Impersonating #{@user.email}
496
+ ```
497
+
498
+ See [docs/users/tramway_custom_pages.md](docs/users/tramway_custom_pages.md) for how the routing/controller/view
499
+ resolution works under the hood.
500
+
433
501
  **route_helper**
434
502
 
435
503
  To get routes Tramway generated just Tramway::Engine.
@@ -1319,6 +1387,15 @@ interactivity.
1319
1387
  Cell content that does not fit its column is truncated with an ellipsis. This is a pure CSS behavior (`truncate` +
1320
1388
  `min-w-0` on each cell) — no JavaScript is involved, and no configuration is needed to enable it.
1321
1389
 
1390
+ Pass `truncate: false` to `tramway_cell` to opt a specific cell out of this behavior and let its content wrap/overflow
1391
+ normally:
1392
+
1393
+ ```erb
1394
+ <%= tramway_cell truncate: false do %>
1395
+ <%= user.bio %>
1396
+ <% end %>
1397
+ ```
1398
+
1322
1399
  On narrow screens all columns always render, and the table scrolls horizontally within itself (the page never
1323
1400
  stretches past the viewport width) so every column stays reachable. See
1324
1401
  [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
@@ -16,6 +16,10 @@ module Tramway
16
16
  ACTIONS = %i[index show new create edit update destroy].freeze
17
17
  RouteStruct = Struct.new(*ACTIONS)
18
18
 
19
+ # Page#action values handled by Tramway's built-in CRUD controller and routing.
20
+ # Any other page is a Custom Page, routed to a host app controller instead.
21
+ BUILT_IN_PAGE_ACTIONS = %w[index show create update destroy].freeze
22
+
19
23
  # HumanName Struct contains human names forms for the entity
20
24
  HumanNameStruct = Struct.new(:single, :plural)
21
25
 
@@ -39,6 +43,14 @@ module Tramway
39
43
  pages.find { |page| page.action == name.to_s }
40
44
  end
41
45
 
46
+ def built_in_pages
47
+ pages.select { |page| BUILT_IN_PAGE_ACTIONS.include?(page.action) }
48
+ end
49
+
50
+ def custom_pages
51
+ pages.reject { |page| BUILT_IN_PAGE_ACTIONS.include?(page.action) }
52
+ end
53
+
42
54
  def show_helper_method
43
55
  build_helper_method(name, route:, namespace:, plural: false)
44
56
  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, &)
@@ -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.1'
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kalashnikovisme