tramway 4.0.1.3 → 4.0.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 +54 -2
- data/app/assets/javascripts/tramway/tramway.js +83 -4
- data/app/components/tramway/nav/item/button_component.html.haml +4 -1
- data/app/components/tramway/nav/item/button_component.rb +2 -1
- data/app/components/tramway/nav/item/link_component.html.haml +4 -1
- data/app/components/tramway/nav/item/link_component.rb +4 -1
- data/app/components/tramway/nav/item_component.rb +10 -2
- data/app/components/tramway/navbar_component.html.haml +20 -11
- data/app/controllers/tramway/entities_controller.rb +35 -6
- data/app/views/tramway/entities/_filters.html.haml +15 -0
- data/app/views/tramway/entities/_list.html.haml +26 -10
- data/config/locales/en.yml +2 -0
- data/config/tailwind.config.js +7 -0
- data/lib/generators/tramway/install/install_generator.rb +5 -3
- data/lib/tramway/configs/entities/page.rb +1 -0
- data/lib/tramway/errors.rb +20 -0
- data/lib/tramway/navbar.rb +15 -7
- 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: d1254075cd4e025f5a4f46d4951fc391bd8c90a4c1a486b3bbe471b0d79cfebb
|
|
4
|
+
data.tar.gz: d37cb6b0408b55f635f75b8cc8f0fb90d5d5efa454a1db5c4303fdf800e45726
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29d854a96abfcadda07911e326932a2347c509a0465dc5e6d4c19533dd7b5299f05aa011d3d928490dc6a17969143dc0490f81206618ecd13bd37040bc5306a3
|
|
7
|
+
data.tar.gz: d176134cef60ad20d9c2617067d0e4c498a966fef4119ef182dd2d13b32ae7919cce4ab2a3d18d076b0a20a7a6a08bda88b667190c072bfd41a2021ffa84c1af
|
data/README.md
CHANGED
|
@@ -261,6 +261,40 @@ In this example, Tramway calls `Campaign.includes(:user, :comments)` before appl
|
|
|
261
261
|
`campaign.user` or `campaign.comments` in the decorator's `index_attributes` does not issue an extra query per row.
|
|
262
262
|
Omitting `includes` produces identical behavior to before this option existed.
|
|
263
263
|
|
|
264
|
+
**filters**
|
|
265
|
+
|
|
266
|
+
Filters are disabled by default on index pages. Enable them by listing the scope names you want selectable under
|
|
267
|
+
`filters` on the `:index` page entry:
|
|
268
|
+
|
|
269
|
+
*config/initializers/tramway.rb*
|
|
270
|
+
```ruby
|
|
271
|
+
Tramway.configure do |config|
|
|
272
|
+
config.entities = [
|
|
273
|
+
{
|
|
274
|
+
name: :campaign,
|
|
275
|
+
namespace: :admin,
|
|
276
|
+
pages: [
|
|
277
|
+
{
|
|
278
|
+
action: :index,
|
|
279
|
+
filters: [:active, :archived]
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
When filters are configured, Tramway adds a "Filters" toggle to the same row as the search box and pagination, above
|
|
288
|
+
the index listing. Clicking it reveals a list of small buttons, one per configured filter, each showing how many
|
|
289
|
+
records currently match it (e.g. "ACTIVE · 34"). Clicking a button navigates to the index page with that filter
|
|
290
|
+
applied, keeping any existing search query. The currently applied filter is highlighted, and the toggle remembers
|
|
291
|
+
whether the list was last left open or closed.
|
|
292
|
+
|
|
293
|
+
Visiting the index page with `?filter=active` calls `Campaign.active` (the scope must already exist on the model) in
|
|
294
|
+
addition to any configured `scope`. If `filter` does not match one of the configured filters, Tramway raises
|
|
295
|
+
`Tramway::Errors::InvalidFilterError` explaining which filters are available. Omitting `filters` (the default) makes
|
|
296
|
+
Tramway ignore `params[:filter]` entirely.
|
|
297
|
+
|
|
264
298
|
**show page**
|
|
265
299
|
|
|
266
300
|
To render a show page for an entity, declare a `:show` action inside the `pages` array in
|
|
@@ -436,7 +470,9 @@ end
|
|
|
436
470
|
|
|
437
471
|
You can inject custom content above an entity's index table by defining an
|
|
438
472
|
`index_header_content` lambda on its decorator. The lambda receives the
|
|
439
|
-
collection of decorated records and can render any component you need.
|
|
473
|
+
collection of decorated records and can render any component you need. See
|
|
474
|
+
[docs/users/tramway_entities.md](docs/users/tramway_entities.md) for where exactly this content is placed on
|
|
475
|
+
the page.
|
|
440
476
|
|
|
441
477
|
*config/initializers/tramway.rb*
|
|
442
478
|
```ruby
|
|
@@ -480,7 +516,7 @@ end
|
|
|
480
516
|
```
|
|
481
517
|
|
|
482
518
|
With this configuration in place, the index page will render the `Create`
|
|
483
|
-
button
|
|
519
|
+
button and the header content above the table of projects.
|
|
484
520
|
|
|
485
521
|
#### Add header content to show pages
|
|
486
522
|
|
|
@@ -947,6 +983,22 @@ end
|
|
|
947
983
|
|
|
948
984
|
will render [this](https://play.tailwindcss.com/UZPTCudFw5)
|
|
949
985
|
|
|
986
|
+
#### Icons on navbar items
|
|
987
|
+
|
|
988
|
+
`nav.item` accepts an optional `icon:` option with a Font Awesome class string, rendered before the item's label:
|
|
989
|
+
|
|
990
|
+
```ruby
|
|
991
|
+
nav.left do
|
|
992
|
+
nav.item 'Users', '/users', icon: 'fa fa-users'
|
|
993
|
+
nav.item 'Podcasts', '/podcasts', icon: 'fa fa-podcast'
|
|
994
|
+
end
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
Icons are optional — items without one just render as plain text links. When the vertical sidebar is collapsed on
|
|
998
|
+
desktop, items rendered with an `icon:` stay visible and clickable as icon-only links (their text label is hidden
|
|
999
|
+
but still available to screen readers via the item's `aria-label`); items without an icon have nothing to show once
|
|
1000
|
+
collapsed.
|
|
1001
|
+
|
|
950
1002
|
#### tramway_navbar
|
|
951
1003
|
|
|
952
1004
|
This helper provides several options. Here is YAML view of `tramway_navbar` options structure
|
|
@@ -457,13 +457,10 @@ class Navbar extends Controller {
|
|
|
457
457
|
|
|
458
458
|
this.hiddenInteractionClasses.forEach((className) => {
|
|
459
459
|
this.desktopHeader.classList.toggle(className, !expanded)
|
|
460
|
-
this.desktopMenu.classList.toggle(className, !expanded)
|
|
461
460
|
})
|
|
462
461
|
|
|
463
462
|
this.desktopHeader.toggleAttribute('inert', !expanded)
|
|
464
|
-
this.desktopMenu.toggleAttribute('inert', !expanded)
|
|
465
463
|
this.desktopHeader.setAttribute('aria-hidden', expanded ? 'false' : 'true')
|
|
466
|
-
this.desktopMenu.setAttribute('aria-hidden', expanded ? 'false' : 'true')
|
|
467
464
|
|
|
468
465
|
this.desktopToggleWrapper.classList.toggle('justify-end', expanded)
|
|
469
466
|
this.desktopToggleWrapper.classList.toggle('justify-center', !expanded)
|
|
@@ -485,4 +482,86 @@ class Navbar extends Controller {
|
|
|
485
482
|
}
|
|
486
483
|
}
|
|
487
484
|
|
|
488
|
-
|
|
485
|
+
class Collapsible extends Controller {
|
|
486
|
+
static targets = ["panel", "icon", "toggle"]
|
|
487
|
+
|
|
488
|
+
connect() {
|
|
489
|
+
this.expanded = true
|
|
490
|
+
|
|
491
|
+
const stored = this.readStored()
|
|
492
|
+
const expanded = stored === null ? true : stored === 'true'
|
|
493
|
+
|
|
494
|
+
this.setExpanded(expanded, { animate: false })
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
toggle(event) {
|
|
498
|
+
event.preventDefault()
|
|
499
|
+
|
|
500
|
+
this.setExpanded(!this.expanded, { persist: true })
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
setExpanded(expanded, { persist = false, animate = true } = {}) {
|
|
504
|
+
this.expanded = expanded
|
|
505
|
+
|
|
506
|
+
if (animate) {
|
|
507
|
+
this.animateTo(expanded)
|
|
508
|
+
} else {
|
|
509
|
+
this.panelTarget.style.height = expanded ? 'auto' : '0px'
|
|
510
|
+
this.panelTarget.style.opacity = expanded ? '1' : '0'
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const expandedCarrier = this.hasToggleTarget ? this.toggleTarget : this.element
|
|
514
|
+
expandedCarrier.setAttribute('aria-expanded', expanded ? 'true' : 'false')
|
|
515
|
+
|
|
516
|
+
if (this.hasIconTarget) {
|
|
517
|
+
this.iconTarget.classList.toggle('rotate-180', !expanded)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (persist) {
|
|
521
|
+
this.writeStored(expanded)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Animates between the panel's current rendered height and its target height, rather than
|
|
526
|
+
// to/from `auto` directly (which cannot be transitioned). `height` is one of the few
|
|
527
|
+
// properties animate.md tolerates outside transform/opacity, since an accordion has no
|
|
528
|
+
// transform equivalent.
|
|
529
|
+
animateTo(expanded) {
|
|
530
|
+
const panel = this.panelTarget
|
|
531
|
+
const targetHeight = expanded ? panel.scrollHeight : 0
|
|
532
|
+
|
|
533
|
+
panel.style.height = `${panel.getBoundingClientRect().height}px`
|
|
534
|
+
panel.getBoundingClientRect() // force reflow so the browser registers the start value
|
|
535
|
+
|
|
536
|
+
requestAnimationFrame(() => {
|
|
537
|
+
panel.style.height = `${targetHeight}px`
|
|
538
|
+
panel.style.opacity = expanded ? '1' : '0'
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
panel.addEventListener('transitionend', () => {
|
|
542
|
+
if (this.expanded) panel.style.height = 'auto'
|
|
543
|
+
}, { once: true })
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
readStored() {
|
|
547
|
+
try {
|
|
548
|
+
return window.localStorage.getItem(this.storageKey)
|
|
549
|
+
} catch {
|
|
550
|
+
return null
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
writeStored(expanded) {
|
|
555
|
+
try {
|
|
556
|
+
window.localStorage.setItem(this.storageKey, expanded ? 'true' : 'false')
|
|
557
|
+
} catch {
|
|
558
|
+
/* localStorage unavailable (e.g. private mode) — state simply won't persist */
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
get storageKey() {
|
|
563
|
+
return `tramway-collapsible-${this.element.dataset.key || 'default'}`
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export { TramwaySelect, UiCheckbox, Tooltip, Navbar, Collapsible }
|
|
@@ -10,7 +10,10 @@ module Tramway
|
|
|
10
10
|
class LinkComponent < Tramway::Nav::ItemComponent
|
|
11
11
|
def initialize(**options)
|
|
12
12
|
@href = options[:href]
|
|
13
|
-
@
|
|
13
|
+
@icon = options[:icon]
|
|
14
|
+
@options = Rules::TurboHtmlAttributesRules.prepare_turbo_html_attributes(
|
|
15
|
+
options: options.except(:href, :icon)
|
|
16
|
+
)
|
|
14
17
|
end
|
|
15
18
|
end
|
|
16
19
|
end
|
|
@@ -7,10 +7,18 @@ module Tramway
|
|
|
7
7
|
class ItemComponent < TailwindComponent
|
|
8
8
|
def style
|
|
9
9
|
@style ||= [
|
|
10
|
-
'tramway-navbar-item inline-flex items-center rounded-md px-4 py-3 text-base font-medium
|
|
11
|
-
'transition-colors hover:bg-zinc-800 hover:text-zinc-50 sm:px-3 sm:py-2 sm:text-sm'
|
|
10
|
+
'tramway-navbar-item inline-flex items-center gap-2 rounded-md px-4 py-3 text-base font-medium',
|
|
11
|
+
'text-zinc-100 transition-colors hover:bg-zinc-800 hover:text-zinc-50 sm:px-3 sm:py-2 sm:text-sm'
|
|
12
12
|
].join(' ')
|
|
13
13
|
end
|
|
14
|
+
|
|
15
|
+
def icon?
|
|
16
|
+
@icon.present?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def icon_classes
|
|
20
|
+
[@icon, 'tramway-navbar-item-icon w-5 shrink-0 text-center'].join(' ')
|
|
21
|
+
end
|
|
14
22
|
end
|
|
15
23
|
end
|
|
16
24
|
end
|
|
@@ -114,21 +114,16 @@
|
|
|
114
114
|
})();
|
|
115
115
|
|
|
116
116
|
:css
|
|
117
|
-
.tramway-navbar-desktop-vertical .
|
|
118
|
-
width: 100%;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.tramway-navbar-desktop-vertical .tramway-navbar-item > a,
|
|
122
|
-
.tramway-navbar-desktop-vertical .tramway-navbar-item > form {
|
|
117
|
+
.tramway-navbar-desktop-vertical li.whitespace-nowrap {
|
|
123
118
|
width: 100%;
|
|
124
119
|
}
|
|
125
120
|
|
|
126
|
-
.tramway-navbar-desktop-vertical .
|
|
121
|
+
.tramway-navbar-desktop-vertical li.whitespace-nowrap > form {
|
|
127
122
|
display: block;
|
|
123
|
+
width: 100%;
|
|
128
124
|
}
|
|
129
125
|
|
|
130
|
-
.tramway-navbar-desktop-vertical .tramway-navbar-item
|
|
131
|
-
.tramway-navbar-desktop-vertical .tramway-navbar-item > form > button {
|
|
126
|
+
.tramway-navbar-desktop-vertical .tramway-navbar-item {
|
|
132
127
|
width: 100%;
|
|
133
128
|
justify-content: flex-start;
|
|
134
129
|
}
|
|
@@ -142,12 +137,26 @@
|
|
|
142
137
|
padding-left: 6rem;
|
|
143
138
|
}
|
|
144
139
|
|
|
145
|
-
html[data-tramway-navbar-expanded="false"] #desktop-navbar-header
|
|
146
|
-
html[data-tramway-navbar-expanded="false"] #desktop-navbar-content {
|
|
140
|
+
html[data-tramway-navbar-expanded="false"] #desktop-navbar-header {
|
|
147
141
|
opacity: 0;
|
|
148
142
|
pointer-events: none;
|
|
149
143
|
}
|
|
150
144
|
|
|
145
|
+
html[data-tramway-navbar-expanded="false"] #desktop-navbar-content {
|
|
146
|
+
opacity: 1;
|
|
147
|
+
pointer-events: auto;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
html[data-tramway-navbar-expanded="false"] #desktop-navbar-content .tramway-navbar-item-label {
|
|
151
|
+
display: none;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
html[data-tramway-navbar-expanded="false"] #desktop-navbar-content .tramway-navbar-item {
|
|
155
|
+
justify-content: center;
|
|
156
|
+
padding-left: 0;
|
|
157
|
+
padding-right: 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
html[data-tramway-navbar-expanded="false"] #desktop-navbar-toggle-wrapper {
|
|
152
161
|
justify-content: center;
|
|
153
162
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'tramway/warnings'
|
|
4
|
+
require 'tramway/errors'
|
|
4
5
|
|
|
5
6
|
module Tramway
|
|
6
7
|
# Main controller for entities pages
|
|
@@ -13,18 +14,15 @@ module Tramway
|
|
|
13
14
|
include Rails.application.routes.url_helpers
|
|
14
15
|
|
|
15
16
|
def index
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
else
|
|
19
|
-
model_class.order(id: :desc)
|
|
20
|
-
end => entities
|
|
21
|
-
|
|
17
|
+
entities = base_entities
|
|
18
|
+
entities = apply_filter(entities)
|
|
22
19
|
entities = preload(entities)
|
|
23
20
|
entities = search(entities)
|
|
24
21
|
entities = entities.page(params[:page])
|
|
25
22
|
@entities = entities
|
|
26
23
|
|
|
27
24
|
@namespace = entity.namespace
|
|
25
|
+
@filter_counts = filter_counts if filters.present?
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
def show
|
|
@@ -91,6 +89,37 @@ module Tramway
|
|
|
91
89
|
entity.page(:index).scope
|
|
92
90
|
end
|
|
93
91
|
|
|
92
|
+
def base_entities
|
|
93
|
+
if index_scope.present?
|
|
94
|
+
model_class.public_send(index_scope)
|
|
95
|
+
else
|
|
96
|
+
model_class.order(id: :desc)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def filters
|
|
101
|
+
entity.page(:index).filters
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def apply_filter(entities)
|
|
105
|
+
return entities if params[:filter].blank? || filters.blank?
|
|
106
|
+
|
|
107
|
+
filter = params[:filter].to_s
|
|
108
|
+
|
|
109
|
+
unless filters.map(&:to_s).include?(filter)
|
|
110
|
+
raise Tramway::Errors::InvalidFilterError.new(filter:, available_filters: filters, model_class:)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
entities.public_send(filter)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def filter_counts
|
|
117
|
+
# Count by :id explicitly: a bare `.count` reuses any custom `select` values from the
|
|
118
|
+
# index page's `scope` (e.g. a raw-SQL aliased subquery column), which breaks the
|
|
119
|
+
# generated SQL. Counting a real column side-steps that entirely.
|
|
120
|
+
filters.index_with { |filter| base_entities.public_send(filter).count(:id) }
|
|
121
|
+
end
|
|
122
|
+
|
|
94
123
|
def preload(entities)
|
|
95
124
|
includes = entity.page(:index).includes
|
|
96
125
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
- current_filter = params[:filter]
|
|
2
|
+
|
|
3
|
+
.overflow-hidden.duration-200.ease-out-strong.motion-reduce:duration-0{ class: 'transition-[height,opacity]', data: { 'tramway--collapsible-target': 'panel' } }
|
|
4
|
+
.flex.flex-wrap.gap-2.mt-2
|
|
5
|
+
- @filter_counts.each do |filter_name, count|
|
|
6
|
+
- filter_params = request.query_parameters.except('page').merge(filter: filter_name)
|
|
7
|
+
- button_type = filter_name.to_s == current_filter ? :primary : :inverse
|
|
8
|
+
= tramway_button text: "#{filter_name.to_s.upcase} · #{count}",
|
|
9
|
+
path: public_send(custom_path_method, filter_params), type: button_type, size: :small
|
|
10
|
+
|
|
11
|
+
- if current_filter.present?
|
|
12
|
+
- clear_params = request.query_parameters.except('page', 'filter')
|
|
13
|
+
= tramway_button tag: :a, path: public_send(custom_path_method, clear_params), type: :inverse,
|
|
14
|
+
size: :small, tooltip: { text: t('tramway.actions.clear_filter'), event: :hover } do
|
|
15
|
+
%i.fa.fa-times{ 'aria-hidden' => 'true' }
|
|
@@ -16,16 +16,32 @@
|
|
|
16
16
|
path: Tramway::Engine.routes.url_helpers.public_send(@entity.routes.new),
|
|
17
17
|
type: :will
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
- if decorator.constantize.index_header_content.present?
|
|
20
|
+
.mt-4
|
|
21
|
+
= decorator.constantize.index_header_content.call(@entities)
|
|
22
|
+
|
|
23
|
+
- filters_key = "#{@entity.namespace}-#{@entity.name}-index-filters" if @filter_counts.present?
|
|
24
|
+
|
|
25
|
+
%div{ data: (@filter_counts.present? ? { controller: 'tramway--collapsible', key: filters_key } : {}) }
|
|
26
|
+
.flex.flex-row.items-center.mt-2.gap-4{ class: (@filter_counts.present? ? 'justify-between' : 'justify-end') }
|
|
27
|
+
- if @filter_counts.present?
|
|
28
|
+
= tramway_button tag: :button, type: :inverse,
|
|
29
|
+
data: { action: 'tramway--collapsible#toggle', 'tramway--collapsible-target': 'toggle' } do
|
|
30
|
+
%i.fa.fa-filter.mr-1{ 'aria-hidden' => 'true' }
|
|
31
|
+
= t('tramway.actions.filters')
|
|
32
|
+
%i.fa.fa-chevron-down.ml-1.transition-transform.duration-200.ease-out-strong.motion-reduce:duration-0{ 'aria-hidden' => 'true', data: { 'tramway--collapsible-target': 'icon' } }
|
|
33
|
+
|
|
34
|
+
.flex.flex-row.items-center.gap-4
|
|
35
|
+
- if @entity.page(:index).search
|
|
36
|
+
= form_with url: public_send(custom_path_method), method: :get, local: true, builder: Tramway::Form::Builder, horizontal: true do |f|
|
|
37
|
+
= f.text_field :query, label: false, value: params[:query], placeholder: t('tramway.actions.search')
|
|
38
|
+
= f.submit nil, 'aria-label' => t('tramway.actions.search') do
|
|
39
|
+
%i.fa.fa-search{ 'aria-hidden' => 'true' }
|
|
40
|
+
|
|
41
|
+
= paginate @entities, custom_path_method:
|
|
42
|
+
|
|
43
|
+
- if @filter_counts.present?
|
|
44
|
+
= render 'tramway/entities/filters', custom_path_method:
|
|
29
45
|
|
|
30
46
|
- if index_attributes.empty?
|
|
31
47
|
%p.text-center.mt-10
|
data/config/locales/en.yml
CHANGED
data/config/tailwind.config.js
CHANGED
|
@@ -6,6 +6,9 @@ const buildUtilities = (prefix, start, end) =>
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
safelist: [
|
|
8
8
|
// === Navbar ===
|
|
9
|
+
'w-5',
|
|
10
|
+
'shrink-0',
|
|
11
|
+
'text-center',
|
|
9
12
|
'ml-4',
|
|
10
13
|
'text-white',
|
|
11
14
|
'md:flex',
|
|
@@ -58,6 +61,7 @@ module.exports = {
|
|
|
58
61
|
'ease-in-out',
|
|
59
62
|
'md:transition-[width]',
|
|
60
63
|
'transition-[padding-left]',
|
|
64
|
+
'transition-[height,opacity]',
|
|
61
65
|
'ease-in-out-strong',
|
|
62
66
|
'md:ease-in-out-strong',
|
|
63
67
|
'ease-out-strong',
|
|
@@ -416,6 +420,7 @@ module.exports = {
|
|
|
416
420
|
'mb-4',
|
|
417
421
|
'mb-2',
|
|
418
422
|
'mt-8',
|
|
423
|
+
'mt-4',
|
|
419
424
|
'mt-2',
|
|
420
425
|
|
|
421
426
|
// === Pagination styles ===
|
|
@@ -464,6 +469,8 @@ module.exports = {
|
|
|
464
469
|
'whitespace-nowrap',
|
|
465
470
|
'h-fit',
|
|
466
471
|
'gap-1',
|
|
472
|
+
'mr-1',
|
|
473
|
+
'ml-1',
|
|
467
474
|
'h-10',
|
|
468
475
|
'px-2.5',
|
|
469
476
|
'py-0.5',
|
|
@@ -115,7 +115,7 @@ module Tramway
|
|
|
115
115
|
|
|
116
116
|
def stimulus_controller_imports
|
|
117
117
|
[
|
|
118
|
-
'import { Navbar, TramwaySelect, UiCheckbox, Tooltip } from "@tramway/tramway"'
|
|
118
|
+
'import { Navbar, TramwaySelect, UiCheckbox, Tooltip, Collapsible } from "@tramway/tramway"'
|
|
119
119
|
]
|
|
120
120
|
end
|
|
121
121
|
|
|
@@ -124,7 +124,8 @@ module Tramway
|
|
|
124
124
|
"application.register('tramway-navbar', Navbar)",
|
|
125
125
|
"application.register('tramway-select', TramwaySelect)",
|
|
126
126
|
"application.register('ui--checkbox', UiCheckbox)",
|
|
127
|
-
"application.register('tramway-tooltip', Tooltip)"
|
|
127
|
+
"application.register('tramway-tooltip', Tooltip)",
|
|
128
|
+
"application.register('tramway--collapsible', Collapsible)"
|
|
128
129
|
]
|
|
129
130
|
end
|
|
130
131
|
|
|
@@ -299,7 +300,8 @@ module Tramway
|
|
|
299
300
|
"application.register('tramway-select'",
|
|
300
301
|
"application.register('table-row-preview'",
|
|
301
302
|
"application.register('ui--checkbox'",
|
|
302
|
-
"application.register('tramway-tooltip'"
|
|
303
|
+
"application.register('tramway-tooltip'",
|
|
304
|
+
"application.register('tramway--collapsible'"
|
|
303
305
|
]
|
|
304
306
|
end
|
|
305
307
|
|
data/lib/tramway/errors.rb
CHANGED
|
@@ -41,5 +41,25 @@ module Tramway
|
|
|
41
41
|
MESSAGE
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
# Raised when `params[:filter]` on an entities#index page does not match any of the
|
|
46
|
+
# filters declared for that page in the Tramway config, so the developer (or the user
|
|
47
|
+
# following a crafted link) gets a clear explanation instead of a raw NoMethodError from
|
|
48
|
+
# calling an arbitrary, possibly non-existent scope.
|
|
49
|
+
class InvalidFilterError < StandardError
|
|
50
|
+
def initialize(filter:, available_filters:, model_class:)
|
|
51
|
+
super(<<~MESSAGE)
|
|
52
|
+
Tramway received an unknown filter "#{filter}" for #{model_class} on the \
|
|
53
|
+
entities#index page.
|
|
54
|
+
|
|
55
|
+
Available filters: #{available_filters.join(', ')}.
|
|
56
|
+
|
|
57
|
+
To fix this:
|
|
58
|
+
- pass one of the available filters above in `params[:filter]`, or
|
|
59
|
+
- add `:#{filter}` to this entity's `filters:` list for the `:index` page in your \
|
|
60
|
+
Tramway config if it should be selectable.
|
|
61
|
+
MESSAGE
|
|
62
|
+
end
|
|
63
|
+
end
|
|
44
64
|
end
|
|
45
65
|
end
|
data/lib/tramway/navbar.rb
CHANGED
|
@@ -35,13 +35,13 @@ module Tramway
|
|
|
35
35
|
reset_filling
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def item(text_or_url, url = nil, **, &)
|
|
38
|
+
def item(text_or_url, url = nil, icon: nil, **, &)
|
|
39
39
|
raise 'You cannot provide an argument and a code block at the same time' if provided_url_and_block?(url, &)
|
|
40
40
|
|
|
41
41
|
if url.present?
|
|
42
|
-
render_ignoring_block(text_or_url, url, **)
|
|
42
|
+
render_ignoring_block(text_or_url, url, icon:, **)
|
|
43
43
|
else
|
|
44
|
-
render_using_block(text_or_url, **, &)
|
|
44
|
+
render_using_block(text_or_url, icon:, **, &)
|
|
45
45
|
end => rendered_item
|
|
46
46
|
|
|
47
47
|
@items[@filling] << rendered_item
|
|
@@ -73,8 +73,9 @@ module Tramway
|
|
|
73
73
|
@filling = nil
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
def render_ignoring_block(text_or_url, url, method: nil, **options)
|
|
77
|
-
options.merge!(href: url)
|
|
76
|
+
def render_ignoring_block(text_or_url, url, method: nil, icon: nil, **options)
|
|
77
|
+
options.merge!(href: url, icon:)
|
|
78
|
+
apply_icon_accessibility_attributes(options, label: text_or_url, icon:)
|
|
78
79
|
|
|
79
80
|
if method.present? && method.to_sym != :get
|
|
80
81
|
context.render(Tramway::Nav::Item::ButtonComponent.new(method:, **options)) { text_or_url }
|
|
@@ -83,8 +84,8 @@ module Tramway
|
|
|
83
84
|
end
|
|
84
85
|
end
|
|
85
86
|
|
|
86
|
-
def render_using_block(text_or_url, method: nil, **options, &)
|
|
87
|
-
options.merge!(href: text_or_url)
|
|
87
|
+
def render_using_block(text_or_url, method: nil, icon: nil, **options, &)
|
|
88
|
+
options.merge!(href: text_or_url, icon:)
|
|
88
89
|
|
|
89
90
|
if method.present? && method.to_sym != :get
|
|
90
91
|
context.render(Tramway::Nav::Item::ButtonComponent.new(method:, **options), &)
|
|
@@ -92,5 +93,12 @@ module Tramway
|
|
|
92
93
|
context.render(Tramway::Nav::Item::LinkComponent.new(method:, **options), &)
|
|
93
94
|
end
|
|
94
95
|
end
|
|
96
|
+
|
|
97
|
+
def apply_icon_accessibility_attributes(options, label:, icon:)
|
|
98
|
+
return if icon.blank?
|
|
99
|
+
|
|
100
|
+
options[:title] = label if options[:title].blank?
|
|
101
|
+
options[:'aria-label'] = label if options[:'aria-label'].blank?
|
|
102
|
+
end
|
|
95
103
|
end
|
|
96
104
|
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: 4.0.
|
|
4
|
+
version: 4.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kalashnikovisme
|
|
@@ -289,6 +289,7 @@ files:
|
|
|
289
289
|
- app/views/kaminari/_prev_page.html.haml
|
|
290
290
|
- app/views/tramway/chats/_message.html.haml
|
|
291
291
|
- app/views/tramway/chats/_messages.html.haml
|
|
292
|
+
- app/views/tramway/entities/_filters.html.haml
|
|
292
293
|
- app/views/tramway/entities/_form.html.haml
|
|
293
294
|
- app/views/tramway/entities/_list.html.haml
|
|
294
295
|
- app/views/tramway/entities/edit.html.haml
|