tramway 0.4.9.3 → 0.5

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -3
  3. data/app/assets/javascripts/tramway/multiselect_controller.js +16 -16
  4. data/app/assets/javascripts/tramway/table_row_preview_controller.js +83 -0
  5. data/app/components/tailwind_component.rb +1 -3
  6. data/app/components/tailwinds/form/label_component.html.haml +2 -0
  7. data/app/components/tailwinds/form/label_component.rb +10 -0
  8. data/app/components/tailwinds/form/multiselect_component.html.haml +2 -2
  9. data/app/components/tailwinds/form/select_component.html.haml +1 -1
  10. data/app/components/tailwinds/form/text_field_component.html.haml +1 -1
  11. data/app/components/tailwinds/nav/item/button_component.rb +1 -1
  12. data/app/components/tailwinds/nav/item/link_component.rb +1 -1
  13. data/app/components/tailwinds/navbar_component.html.haml +34 -12
  14. data/app/components/tailwinds/pagination/base.rb +22 -0
  15. data/app/components/tailwinds/pagination/first_page_component.html.haml +3 -0
  16. data/app/components/tailwinds/pagination/first_page_component.rb +9 -0
  17. data/app/components/tailwinds/pagination/gap_component.html.haml +2 -0
  18. data/app/components/tailwinds/pagination/gap_component.rb +9 -0
  19. data/app/components/tailwinds/pagination/last_page_component.html.haml +3 -0
  20. data/app/components/tailwinds/pagination/last_page_component.rb +9 -0
  21. data/app/components/tailwinds/pagination/next_page_component.html.haml +3 -0
  22. data/app/components/tailwinds/pagination/next_page_component.rb +9 -0
  23. data/app/components/tailwinds/pagination/page_component.html.haml +5 -0
  24. data/app/components/tailwinds/pagination/page_component.rb +10 -0
  25. data/app/components/tailwinds/pagination/prev_page_component.html.haml +3 -0
  26. data/app/components/tailwinds/pagination/prev_page_component.rb +9 -0
  27. data/app/components/tailwinds/table/cell_component.html.haml +2 -0
  28. data/app/components/tailwinds/table/cell_component.rb +9 -0
  29. data/app/components/tailwinds/table/header_component.html.haml +6 -0
  30. data/app/components/tailwinds/table/header_component.rb +10 -0
  31. data/app/components/tailwinds/table/row/preview_component.html.haml +18 -0
  32. data/app/components/tailwinds/table/row/preview_component.rb +11 -0
  33. data/app/components/tailwinds/table/row_component.html.haml +24 -0
  34. data/app/components/tailwinds/table/row_component.rb +25 -0
  35. data/app/components/tailwinds/table_component.html.haml +4 -0
  36. data/app/components/tailwinds/table_component.rb +7 -0
  37. data/app/components/tramway/component/base.rb +11 -0
  38. data/app/controllers/tramway/entities_controller.rb +19 -0
  39. data/app/helpers/tramway/application_helper.rb +15 -0
  40. data/app/views/kaminari/_first_page.html.haml +1 -9
  41. data/app/views/kaminari/_gap.html.haml +1 -8
  42. data/app/views/kaminari/_last_page.html.haml +1 -9
  43. data/app/views/kaminari/_next_page.html.haml +1 -9
  44. data/app/views/kaminari/_page.html.haml +1 -14
  45. data/app/views/kaminari/_prev_page.html.haml +1 -9
  46. data/app/views/tramway/entities/_entity.html.haml +2 -0
  47. data/app/views/tramway/entities/index.html.haml +22 -0
  48. data/config/routes.rb +3 -2
  49. data/lib/tramway/base_decorator.rb +6 -0
  50. data/lib/tramway/config.rb +2 -1
  51. data/lib/tramway/configs/entity.rb +21 -8
  52. data/lib/tramway/decorators/class_helper.rb +17 -8
  53. data/lib/tramway/helpers/component_helper.rb +12 -0
  54. data/lib/tramway/version.rb +1 -1
  55. metadata +48 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a319164d807e5ed06f1a2a94f0fc4158850e83134390b65c1567f0192bb56bc8
4
- data.tar.gz: 21a85ecb9c14cbd30d6ebe145289917a8d935a36039813274b2d3a5b7992e723
3
+ metadata.gz: 7383199c539bbfdb97568068d0568fbf9366f2d3d6069258f2b96182c31bf2b4
4
+ data.tar.gz: d2320192e25efa23450e4e33730d869fe6c94b8bf8af885a494c4dfc5e808c44
5
5
  SHA512:
6
- metadata.gz: 418663e713b81978e3026d4be7e0a671936564957bdede23c9a979f0f8a525c558eda982f668e6e59ed01ec29b03ccadf80b393eea414a4f246c251532fabee6
7
- data.tar.gz: f67485a339908dbea9ff9a95054c645196d3e3d58bfe678487a8652925e35f110167acbd84ead6e3dab362162f8e49de1290b093e15dd4022fe9d4407183e350
6
+ metadata.gz: 70f4ea1f899fbf291fafb530129d5b4778b114d7f4c17115192c0f90a35269f871e7d23cd494361b175811d50542b746079abd0e1aacdd65963eb892f984f25b
7
+ data.tar.gz: 3db27c125714d2f67500e703fe10bc3497926672b943b34ea9d222e1832a7976b589cb0e502b0ab7a8400fb9616c784007c109facbf52f859e072bc51291420f
data/README.md CHANGED
@@ -539,9 +539,7 @@ user_2.object #=> returns pure user object
539
539
  Install [lefthook](https://github.com/evilmartians/lefthook)
540
540
 
541
541
  ```
542
- bundle
543
- lefthook install
544
- rspec
542
+ make install
545
543
  ```
546
544
 
547
545
  ## License
@@ -17,22 +17,25 @@ export default class Multiselect extends Controller {
17
17
 
18
18
  connect() {
19
19
  this.dropdownState = 'closed';
20
- this.unselectedItems = JSON.parse(this.element.dataset.items).map((item) => {
20
+ this.items = JSON.parse(this.element.dataset.items).map((item, index) => {
21
21
  return {
22
+ index,
22
23
  text: item.text,
23
- value: item.value.toString()
24
+ value: item.value.toString(),
25
+ selected: false
24
26
  }
25
27
  });
26
28
 
27
29
  const initialValues = this.element.dataset.value === undefined ? [] : this.element.dataset.value.split(',')
28
- this.selectedItems = this.unselectedItems.filter(item => initialValues.includes(item.value));
29
- this.unselectedItems = this.unselectedItems.filter(item => !initialValues.includes(item.value));
30
+ this.selectedItems = this.items.filter(item => initialValues.includes(item.value));
31
+ this.items = this.items.filter(item => !initialValues.includes(item.value));
30
32
 
31
33
  this.renderSelectedItems();
32
34
  }
33
35
 
34
36
  renderSelectedItems() {
35
37
  const allItems = this.fillTemplate(this.element.dataset.selectedItemTemplate, this.selectedItems);
38
+
36
39
  this.showSelectedAreaTarget.innerHTML = allItems;
37
40
  this.showSelectedAreaTarget.insertAdjacentHTML("beforeEnd", this.input());
38
41
  this.updateInputOptions();
@@ -86,25 +89,22 @@ export default class Multiselect extends Controller {
86
89
  get template() {
87
90
  return this.element.dataset.dropdownContainer.replace(
88
91
  /{{content}}/g,
89
- this.fillTemplate(this.element.dataset.itemContainer, this.unselectedItems)
92
+ this.fillTemplate(this.element.dataset.itemContainer, this.items.filter(item => !item.selected))
90
93
  );
91
94
  }
92
95
 
93
96
  toggleItem({ currentTarget }) {
94
- const item = {
95
- text: currentTarget.dataset.text,
96
- value: currentTarget.dataset.value
97
- };
98
-
99
- const itemIndex = this.selectedItems.findIndex(x => x.value === item.value);
100
- if (itemIndex !== -1) {
101
- this.selectedItems = this.selectedItems.filter((_, index) => index !== itemIndex);
97
+ const itemIndex = this.items.findIndex(x => x.value === currentTarget.dataset.value);
98
+ const itemSelectedIndex = this.selectedItems.findIndex(x => x.value === currentTarget.dataset.value);
99
+
100
+ if (itemSelectedIndex !== -1) {
101
+ this.selectedItems = this.selectedItems.filter((_, index) => index !== itemSelectedIndex);
102
+ this.items[itemIndex].selected = false;
102
103
  } else {
103
- this.selectedItems.push(item);
104
+ this.selectedItems.push(this.items[itemIndex]);
105
+ this.items[itemIndex].selected = true;
104
106
  }
105
107
 
106
- this.unselectedItems = this.unselectedItems.filter(x => x.value !== item.value);
107
-
108
108
  this.renderSelectedItems();
109
109
  this.rerenderItems();
110
110
  }
@@ -0,0 +1,83 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class TableRowPreview extends Controller {
4
+ connect() {
5
+ this.items = JSON.parse(this.element.dataset.items || '{}');
6
+ }
7
+
8
+ toggle() {
9
+ const rollUp = document.getElementById("roll-up");
10
+
11
+ const existingTable = rollUp.querySelector(".div-table");
12
+ if (existingTable) {
13
+ existingTable.remove();
14
+ }
15
+
16
+ const existingTitle = rollUp.querySelector("h3");
17
+ if (existingTitle) {
18
+ existingTitle.remove();
19
+ }
20
+
21
+ if (Object.keys(this.items).length === 0) return;
22
+
23
+ const titleText = document.createElement("h3");
24
+
25
+ titleText.classList.add("text-xl");
26
+ titleText.classList.add("text-white");
27
+ titleText.classList.add("py-4");
28
+ titleText.classList.add("px-4");
29
+ titleText.textContent = Object.values(this.items)[0];
30
+
31
+ const table = this.createTable(this.items);
32
+
33
+ rollUp.insertAdjacentElement('afterbegin', table);
34
+ rollUp.insertAdjacentElement('afterbegin', titleText);
35
+
36
+ rollUp.classList.toggle("hidden");
37
+ }
38
+
39
+ close() {
40
+ const rollUp = document.getElementById("roll-up");
41
+ rollUp.classList.add("hidden");
42
+ }
43
+
44
+ createTable(items) {
45
+ const table = document.createElement("div");
46
+ table.classList.add("div-table");
47
+ table.classList.add("text-white");
48
+ table.classList.add("px-2");
49
+
50
+ Object.entries(items).forEach(([key, value]) => {
51
+ const rows = this.createTableRow(key, value);
52
+
53
+ rows.forEach((row) => table.appendChild(row));
54
+ });
55
+
56
+ return table;
57
+ }
58
+
59
+ createTableRow(key, value) {
60
+ const keyRow = document.createElement("div");
61
+ keyRow.classList.add("div-table-row");
62
+ keyRow.classList.add("bg-purple-300");
63
+ keyRow.classList.add("text-purple-700");
64
+ keyRow.classList.add("dark:text-white");
65
+ keyRow.classList.add("dark:bg-gray-700");
66
+ keyRow.classList.add("px-2");
67
+ keyRow.classList.add("py-1");
68
+ keyRow.classList.add("text-xs");
69
+ keyRow.classList.add("font-semibold");
70
+ keyRow.textContent = key;
71
+
72
+ const valueRow = document.createElement("div");
73
+ valueRow.classList.add("div-table-row");
74
+ valueRow.classList.add("dark:bg-gray-800");
75
+ valueRow.classList.add("px-2");
76
+ valueRow.classList.add("py-2");
77
+ valueRow.textContent = value;
78
+
79
+ return [keyRow, valueRow];
80
+ }
81
+ }
82
+
83
+ export { TableRowPreview };
@@ -3,9 +3,7 @@
3
3
  require 'view_component'
4
4
 
5
5
  # Base TailwindComponent. Contains base features for all tailwind components
6
- class TailwindComponent < ViewComponent::Base
7
- extend Dry::Initializer[undefined: false]
8
-
6
+ class TailwindComponent < Tramway::Component::Base
9
7
  option :input
10
8
  option :attribute
11
9
  option :value, optional: true
@@ -0,0 +1,2 @@
1
+ %label.block.text-gray-700.text-sm.font-bold.mb-2.dark:text-white{ for: }
2
+ = content
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Form
5
+ # Form label for all tailwind-styled forms
6
+ class LabelComponent < Tramway::Component::Base
7
+ option :for
8
+ end
9
+ end
10
+ end
@@ -1,8 +1,8 @@
1
1
  .mb-4
2
2
  - if @label
3
- %label.block.text-gray-700.text-sm.font-bold.mb-2{ for: @for }
3
+ = component('tailwinds/form/label', for: @for) do
4
4
  = @label
5
- .flex.flex-col.items-center.relative{ data: multiselect_hash, id: "#{@for}_multiselect" }
5
+ .flex.flex-col.relative{ data: multiselect_hash, id: "#{@for}_multiselect" }
6
6
  .min-w-96.w-fit
7
7
  .p-1.flex.border.border-gray-200.bg-white.rounded{ data: { "multiselect-target" => "dropdown" } }
8
8
  .flex.flex-auto.flex-wrap{ data: { "multiselect-target" => "showSelectedArea" } }
@@ -1,5 +1,5 @@
1
1
  .mb-4
2
2
  - if @label
3
- %label.block.text-gray-700.text-sm.font-bold.mb-2{ for: @for }
3
+ = component('tailwinds/form/label', for: @for) do
4
4
  = @label
5
5
  = @input.call(@attribute, @collection, { selected: @value }, @options.merge(class: 'bg-white border border-gray-300 text-gray-700 py-2.5 px-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent'))
@@ -1,5 +1,5 @@
1
1
  .mb-4
2
2
  - if @label
3
- %label.block.text-gray-700.text-sm.font-bold.mb-2{ for: @for }
3
+ = component('tailwinds/form/label', for: @for) do
4
4
  = @label
5
5
  = @input.call @attribute, **@options.merge(class: 'w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-red-500'), value: @value
@@ -11,7 +11,7 @@ module Tailwinds
11
11
  def initialize(**options)
12
12
  @href = options[:href]
13
13
  @method = options[:method]
14
- @style = 'text-white hover:bg-red-300 px-4 py-2 rounded'
14
+ @style = 'text-white hover:bg-gray-300 hover:text-gray-800 px-4 py-2 rounded whitespace-nowrap'
15
15
  @options = options.except(:href, :method)
16
16
  end
17
17
  end
@@ -10,7 +10,7 @@ module Tailwinds
10
10
  class LinkComponent < TailwindComponent
11
11
  def initialize(**options)
12
12
  @href = options[:href]
13
- @style = 'text-white hover:bg-red-300 px-4 py-2 rounded'
13
+ @style = 'text-white hover:bg-gray-300 hover:text-gray-800 px-4 py-2 rounded whitespace-nowrap'
14
14
  @options = Rules::TurboHtmlAttributesRules.prepare_turbo_html_attributes(options:)
15
15
  end
16
16
  end
@@ -1,16 +1,38 @@
1
- %nav.py-4.px-8.flex.justify-between.items-center{ class: "bg-#{@color}" }
2
- - if @title[:text].present? || @left_items.present?
3
- .flex
4
- - if @title[:text].present?
5
- = link_to @title[:link] do
6
- .text-xl.text-white.font-bold
7
- = @title[:text]
8
- - if @left_items.present?
9
- %ul.flex.items-center.space-x-4
10
- - @left_items.each do |item|
11
- = item
1
+ %nav.py-4.px-4.sm:px-8.flex.justify-between.items-center.dark:bg-gray-800{ class: "bg-#{@color}" }
2
+ .flex.justify-between.w-full
3
+ - if @title[:text].present? || @left_items.present?
4
+ .flex.items-center
5
+ - if @title[:text].present?
6
+ = link_to @title[:link] do
7
+ .text-xl.text-white.font-bold
8
+ = @title[:text]
9
+ - if @left_items.present?
10
+ %ul.hidden.sm:flex.items-center.space-x-4.ml-4
11
+ - @left_items.each do |item|
12
+ = item
13
+
14
+ .block.sm:hidden
15
+ %button#mobile-menu-button.text-white.focus:outline-none
16
+
17
+
18
+ - if @right_items.present?
19
+ %ul.hidden.sm:flex.items-center.space-x-4
20
+ - @right_items.each do |item|
21
+ = item
22
+
23
+ #mobile-menu.hidden.flex-col.sm:hidden.dark:bg-gray-800
24
+ - if @left_items.present?
25
+ %ul.flex.flex-col.space-y-2
26
+ - @left_items.each do |item|
27
+ = item
12
28
 
13
29
  - if @right_items.present?
14
- %ul.flex.items-center.space-x-4
30
+ %ul.flex.flex-col.space-y-2
15
31
  - @right_items.each do |item|
16
32
  = item
33
+
34
+ :javascript
35
+ document.getElementById('mobile-menu-button').addEventListener('click', function() {
36
+ var menu = document.getElementById('mobile-menu');
37
+ menu.classList.toggle('hidden');
38
+ });
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Pagination
5
+ # Base component for rendering a Kaminari pagination
6
+ class Base < Tramway::Component::Base
7
+ option :current_page
8
+ option :url
9
+ option :remote
10
+
11
+ # :reek:UtilityFunction { enabled: false }
12
+ def pagination_classes(klass: nil)
13
+ default_classes = ['cursor-pointer', 'px-3', 'py-2', 'font-medium', 'text-purple-700', 'bg-white',
14
+ 'rounded-md', 'hover:bg-purple-100', 'dark:text-white', 'dark:bg-gray-800',
15
+ 'dark:hover:bg-gray-700']
16
+
17
+ (default_classes + [klass]).join(' ')
18
+ end
19
+ # :reek:UtilityFunction { enabled: true }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ - unless current_page.first?
2
+ = link_to helpers.t('views.pagination.first').html_safe, url, remote:, class: pagination_classes(klass: 'first hidden sm:flex')
3
+ = link_to '⭰', url, remote:, class: pagination_classes(klass: 'first sm:hidden font-bold')
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Pagination
5
+ # Kaminari first page component for rendering a first page button in a pagination
6
+ class FirstPageComponent < Tailwinds::Pagination::Base
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,2 @@
1
+ %span.page.gap.px-3.py-2.text-sm.font-medium.text-purple-700.dark:text-white.sm:flex.hidden
2
+ = helpers.t('views.pagination.truncate').html_safe
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Pagination
5
+ # Kaminari gap component for rendering a gap in a pagination
6
+ class GapComponent < Tramway::Component::Base
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ - unless current_page.last?
2
+ = link_to helpers.t('views.pagination.last').html_safe, url, remote:, class: pagination_classes(klass: 'last hidden sm:flex')
3
+ = link_to '⭲', url, remote:, class: pagination_classes(klass: 'last sm:hidden font-bold')
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Pagination
5
+ # Kaminari next page component for rendering a last page button in a pagination
6
+ class LastPageComponent < Tailwinds::Pagination::Base
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ - unless current_page.last?
2
+ = link_to helpers.t('views.pagination.next').html_safe, url, rel: 'next', remote:, class: pagination_classes(klass: 'next hidden sm:flex')
3
+ = link_to '🠖', url, rel: 'next', remote:, class: pagination_classes(klass: 'next sm:hidden font-bold')
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Pagination
5
+ # Kaminari next page component for rendering a next page button in a pagination
6
+ class NextPageComponent < Tailwinds::Pagination::Base
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ - if page.current?
2
+ %span.px-3.py-2.font-medium.rounded-md.bg-purple-500.text-white.dark:text-gray-800.dark:bg-white
3
+ = page
4
+ - else
5
+ = link_to page, url, remote:, rel: page.rel, class: pagination_classes(klass: 'hidden sm:flex')
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Pagination
5
+ # Kaminari page component for rendering a page button in a pagination
6
+ class PageComponent < Tailwinds::Pagination::Base
7
+ option :page
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ - unless current_page.first?
2
+ = link_to helpers.t('views.pagination.previous').html_safe, url, rel: 'prev', remote:, class: pagination_classes(klass: 'prev hidden sm:flex')
3
+ = link_to '🠔', url, rel: 'prev', remote:, class: pagination_classes(klass: 'prev sm:hidden font-bold')
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Pagination
5
+ # Kaminari prev page component for rendering a prev page button in a pagination
6
+ class PrevPageComponent < Tailwinds::Pagination::Base
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,2 @@
1
+ .div-table-cell.px-6.py-4.font-medium.text-gray-900.whitespace-nowrap.dark:bg-gray-800.dark:text-white.text-xs.sm:text-base
2
+ = content
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Table
5
+ # Component for rendering a cell in a table
6
+ class CellComponent < Tramway::Component::Base
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ - cols = headers.map { |item| "1fr" }.join(",")
2
+
3
+ .div-table-row.hidden.md:grid.text-white.text-small.gap-4.bg-purple-700.dark:bg-gray-700.dark:text-gray-400{ class: "grid-cols-[#{cols}]" }
4
+ - headers.each do |header|
5
+ .div-table-cell.py-4.px-6
6
+ = header
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Table
5
+ # Component for rendering a header in a table
6
+ class HeaderComponent < Tramway::Component::Base
7
+ option :headers
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,18 @@
1
+ :css
2
+ @keyframes roll-up {
3
+ 0% {
4
+ transform: translateY(100%);
5
+ }
6
+
7
+ 100% {
8
+ transform: translateY(0%);
9
+ }
10
+ }
11
+
12
+ .animate-roll-up {
13
+ animation: roll-up 0.5s ease-in-out;
14
+ }
15
+
16
+ #roll-up.fixed.hidden.inset-x-0.bottom-0.bg-purple-700.shadow-lg.z-50.dark:bg-gray-800.animate-roll-up{ class: 'h-1/2' }
17
+ %button.absolute.top-4.text-white.right-4.hover:text-gray-700.dark:text-gray-400.dark:hover:text-gray-200{ "data-action" => "click->preview#close", "data-controller" => "preview" }
18
+ &#x2715;
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Table
5
+ module Row
6
+ # Row component for rendering a row in a table
7
+ class PreviewComponent < Tramway::Component::Base
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,24 @@
1
+
2
+ - if cells.any?
3
+ - cols = cells.count.times.map { |item| "1fr" }.join(",")
4
+
5
+ = row_tag class: "div-table-row hidden md:grid gap-4 bg-white border-b dark:bg-gray-800 dark:border-gray-700 grid-cols-[#{cols}]" do
6
+ - cells.each do |(_, value)|
7
+ .div-table-cell.px-6.py-4.font-medium.text-gray-900.whitespace-nowrap.dark:text-white.text-xs.sm:text-base
8
+ = value
9
+
10
+ .div-table-row.border-b.dark:bg-gray-800.dark:border-gray-700.md:hidden.mb-2{ "data-action" => "click->preview#toggle", "data-controller" => "preview", "data-items" => cells.to_json }
11
+ .w-full.p-4.bg-purple-100.text-gray-700.dark:bg-gray-700.dark:text-gray-400
12
+ = cells.values.first
13
+
14
+ .flex.overflow-x-auto.whitespace-nowrap
15
+ - cells.each_with_index do |(_, value), index|
16
+ - next if index == 0
17
+
18
+ .text-gray-900.dark:text-white.p-4.text-xs.sm:text-base.inline-block.w-auto
19
+ = value
20
+ - else
21
+ - cols = Nokogiri::HTML.fragment(content).children.css('> div').count.times.map { |item| "1fr" }.join(",")
22
+
23
+ %div{ class: "div-table-row hidden md:grid gap-4 bg-white border-b dark:bg-gray-800 dark:border-gray-700 grid-cols-[#{cols}]" }
24
+ = content
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Table
5
+ # Component for rendering a row in a table
6
+ class RowComponent < Tramway::Component::Base
7
+ option :cells, optional: true, default: -> { [] }
8
+ option :href, optional: true
9
+
10
+ def row_tag(**options, &)
11
+ if href.present?
12
+ klass = "#{options[:class] || ''} cursor-pointer hover:bg-gray-700"
13
+
14
+ link_to(href, options.merge(class: klass)) do
15
+ yield if block_given?
16
+ end
17
+ else
18
+ tag.div(**options) do
19
+ yield if block_given?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ = helpers.component 'tailwinds/table/row/preview'
2
+
3
+ .div-table.w-full.text-left.rtl:text-right.text-gray-500.dark:text-gray-400.mt-4
4
+ = content
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ # Table component for rendering a table
5
+ class TableComponent < Tramway::Component::Base
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Component
5
+ # You can use this class as a base for all your components
6
+ class Base < ViewComponent::Base
7
+ extend Dry::Initializer[undefined: false]
8
+ include Tramway::Helpers::ComponentHelper
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ # Main controller for entities pages
5
+ class EntitiesController < Tramway.config.application_controller.constantize
6
+ helper Tramway::ApplicationHelper
7
+ include Rails.application.routes.url_helpers
8
+
9
+ def index
10
+ @entities = model_class.page(params[:page])
11
+ end
12
+
13
+ private
14
+
15
+ def model_class
16
+ @model_class ||= params[:entity].classify.constantize
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tramway/helpers/component_helper'
4
+
5
+ module Tramway
6
+ # Main helper module for Tramway entities pages
7
+ module ApplicationHelper
8
+ include Tramway::Decorators::ClassHelper
9
+ include Tramway::Helpers::ComponentHelper
10
+
11
+ def page_title
12
+ @model_class.model_name.human.pluralize
13
+ end
14
+ end
15
+ end
@@ -1,9 +1 @@
1
- -# Link to the "First" page
2
- -# available local variables
3
- -# url: url to the first page
4
- -# current_page: a page object for the currently displayed page
5
- -# total_pages: total number of pages
6
- -# per_page: number of items to fetch per page
7
- -# remote: data-remote
8
- %span.first{ class: 'px-3 py-2 text-sm font-medium text-purple-700 bg-white rounded-md hover:bg-purple-100' }
9
- = link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote
1
+ = component 'tailwinds/pagination/first_page', current_page:, url:, remote:
@@ -1,8 +1 @@
1
- -# Non-link tag that stands for skipped pages...
2
- -# available local variables
3
- -# current_page: a page object for the currently displayed page
4
- -# total_pages: total number of pages
5
- -# per_page: number of items to fetch per page
6
- -# remote: data-remote
7
- %span.page.gap{ class: 'px-3 py-2 text-sm font-medium text-purple-700' }
8
- = t('views.pagination.truncate').html_safe
1
+ = component 'tailwinds/pagination/gap'
@@ -1,9 +1 @@
1
- -# Link to the "Last" page
2
- -# available local variables
3
- -# url: url to the last page
4
- -# current_page: a page object for the currently displayed page
5
- -# total_pages: total number of pages
6
- -# per_page: number of items to fetch per page
7
- -# remote: data-remote
8
- %span.last{ class: 'px-3 py-2 text-sm font-medium text-purple-700 bg-white rounded-md hover:bg-purple-100' }
9
- = link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, remote: remote
1
+ = component 'tailwinds/pagination/last_page', current_page:, url:, remote:
@@ -1,9 +1 @@
1
- -# Link to the "Next" page
2
- -# available local variables
3
- -# url: url to the next page
4
- -# current_page: a page object for the currently displayed page
5
- -# total_pages: total number of pages
6
- -# per_page: number of items to fetch per page
7
- -# remote: data-remote
8
- %span.next{ class: 'px-3 py-2 text-sm font-medium text-purple-700 bg-white rounded-md hover:bg-purple-100' }
9
- = link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote
1
+ = component 'tailwinds/pagination/next_page', current_page:, url:, remote:
@@ -1,14 +1 @@
1
- -# Link showing page number
2
- -# available local variables
3
- -# page: a page object for "this" page
4
- -# url: url to this page
5
- -# current_page: a page object for the currently displayed page
6
- -# total_pages: total number of pages
7
- -# per_page: number of items to fetch per page
8
- -# remote: data-remote
9
- - if page.current?
10
- %span{class: "px-3 py-2 font-medium rounded-md bg-purple-500 text-white" }
11
- = page
12
- - else
13
- %span{class: "cursor px-3 py-2 font-medium text-purple-700 bg-white rounded-md hover:bg-purple-100"}
14
- = link_to_unless page.current?, page, url, {remote: remote, rel: page.rel}
1
+ = component 'tailwinds/pagination/page', current_page:, url:, remote:, page:
@@ -1,9 +1 @@
1
- -# Link to the "Previous" page
2
- -# available local variables
3
- -# url: url to the previous page
4
- -# current_page: a page object for the currently displayed page
5
- -# total_pages: total number of pages
6
- -# per_page: number of items to fetch per page
7
- -# remote: data-remote
8
- %span.prev{ class: 'px-3 py-2 text-sm font-medium text-purple-700 bg-white rounded-md hover:bg-purple-100' }
9
- = link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote
1
+ = component 'tailwinds/pagination/prev_page', current_page:, url:, remote:
@@ -0,0 +1,2 @@
1
+ - decorator = decorator_class(entity)
2
+ = component 'tailwinds/table/row', cells: decorator.list_attributes.reduce({}) { |hash, attribute| hash.merge! attribute => entity.public_send(attribute) }, href: decorator.decorate(entity).show_path
@@ -0,0 +1,22 @@
1
+ .w-full
2
+ - content_for :title, page_title
3
+
4
+ .flex.justify-between.items-center.md:mt-4.mt-2
5
+ %h1.font-bold.text-4xl.dark:text-white
6
+ = page_title
7
+
8
+ - if Tramway.config.pagination[:enabled]
9
+ = paginate @entities
10
+
11
+ - if decorator_class(@entities).list_attributes.empty?
12
+ %p.text-center.mt-10
13
+ You should fill class-level method `self.list_attributes` inside your
14
+ = decorator_class_name(@entities)
15
+
16
+ = component 'tailwinds/table' do
17
+ = component 'tailwinds/table/header', headers: decorator_class(@entities).list_attributes.map { |attribute| @model_class.human_attribute_name(attribute) }
18
+ - @entities.each do |item|
19
+ = render 'entity', entity: item
20
+
21
+ .flex.mt-4
22
+ = paginate @entities
data/config/routes.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Lint/EmptyBlock
4
3
  Tramway::Engine.routes.draw do
4
+ Tramway.config.entities.each do |entity|
5
+ resources entity.name.pluralize.to_sym, only: entity.pages, controller: :entities, defaults: { entity: entity.name }
6
+ end
5
7
  end
6
- # rubocop:enable Lint/EmptyBlock
@@ -43,6 +43,10 @@ module Tramway
43
43
  end
44
44
  end
45
45
 
46
+ def list_attributes
47
+ []
48
+ end
49
+
46
50
  include Tramway::Decorators::AssociationClassMethods
47
51
  end
48
52
 
@@ -51,5 +55,7 @@ module Tramway
51
55
 
52
56
  "#{underscored_class_name.pluralize}/#{underscored_class_name}"
53
57
  end
58
+
59
+ def show_path = nil
54
60
  end
55
61
  end
@@ -12,7 +12,8 @@ module Tramway
12
12
 
13
13
  attr_config(
14
14
  pagination: { enabled: false },
15
- entities: []
15
+ entities: [],
16
+ application_controller: 'ActionController::Base'
16
17
  )
17
18
 
18
19
  def entities=(collection)
@@ -7,6 +7,7 @@ module Tramway
7
7
  # Tramway is an entity-based framework
8
8
  class Entity < Dry::Struct
9
9
  attribute :name, Types::Coercible::String
10
+ attribute? :pages, Types::Array.of(Types::Symbol).default([].freeze)
10
11
  attribute? :route, Tramway::Configs::Entities::Route
11
12
 
12
13
  # Route Struct contains implemented in Tramway CRUD and helpful routes for the entity
@@ -16,13 +17,13 @@ module Tramway
16
17
  HumanNameStruct = Struct.new(:single, :plural)
17
18
 
18
19
  def routes
19
- RouteStruct.new(Rails.application.routes.url_helpers.public_send(route_helper_method))
20
+ RouteStruct.new(route_helper_method)
20
21
  end
21
22
 
22
23
  def human_name
23
24
  single, plural = if model_class.present?
24
25
  model_name = model_class.model_name.human
25
- [model_name, model_name.pluralize]
26
+ [model_name, pluralized(model_name)]
26
27
  else
27
28
  [name.capitalize, name.pluralize.capitalize]
28
29
  end
@@ -32,8 +33,14 @@ module Tramway
32
33
 
33
34
  private
34
35
 
36
+ def pluralized(model_name)
37
+ local_plural = I18n.t("#{name}.many", scope: 'activerecord.plural.models', default: nil)
38
+
39
+ local_plural.present? ? local_plural : model_name.pluralize
40
+ end
41
+
35
42
  def model_class
36
- name.camelize.constantize
43
+ name.classify.constantize
37
44
  rescue StandardError
38
45
  nil
39
46
  end
@@ -41,11 +48,17 @@ module Tramway
41
48
  def route_helper_method
42
49
  underscored_name = name.parameterize.pluralize.underscore
43
50
 
44
- if route.present?
45
- route.helper_method_by(underscored_name)
46
- else
47
- "#{underscored_name}_path"
48
- end
51
+ method_name = if pages.include?(:index) || route.blank?
52
+ "#{underscored_name}_path"
53
+ else
54
+ route.helper_method_by(underscored_name)
55
+ end
56
+
57
+ route_helper_engine.routes.url_helpers.public_send(method_name)
58
+ end
59
+
60
+ def route_helper_engine
61
+ pages.include?(:index) ? Tramway::Engine : Rails.application
49
62
  end
50
63
  end
51
64
  end
@@ -6,19 +6,28 @@ module Tramway
6
6
  module ClassHelper
7
7
  module_function
8
8
 
9
- def decorator_class(object_or_array, decorator)
9
+ def decorator_class(object_or_array, decorator = nil)
10
10
  if decorator.present?
11
11
  decorator
12
12
  else
13
- klass = if Tramway::Decorators::CollectionDecorators.collection?(object_or_array)
14
- object_or_array.first.class
15
- else
16
- object_or_array.class
17
- end
18
-
19
- Tramway::Decorators::NameBuilder.default_decorator_class_name(klass).constantize
13
+ begin
14
+ class_name = decorator_class_name(object_or_array)
15
+ class_name.constantize
16
+ rescue NameError
17
+ raise NameError, "You should define #{class_name} decorator class."
18
+ end
20
19
  end
21
20
  end
21
+
22
+ def decorator_class_name(object_or_array)
23
+ klass = if Tramway::Decorators::CollectionDecorators.collection?(object_or_array)
24
+ object_or_array.first.class
25
+ else
26
+ object_or_array.class
27
+ end
28
+
29
+ Tramway::Decorators::NameBuilder.default_decorator_class_name(klass)
30
+ end
22
31
  end
23
32
  end
24
33
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Helpers
5
+ # Provides a helper method `component` that allows to render components conveniently
6
+ module ComponentHelper
7
+ def component(name, *, **, &)
8
+ render("#{name}_component".classify.constantize.new(*, **), &)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tramway
4
- VERSION = '0.4.9.3'
4
+ VERSION = '0.5'
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: 0.4.9.3
4
+ version: '0.5'
5
5
  platform: ruby
6
6
  authors:
7
7
  - kalashnikovisme
@@ -9,8 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-11-19 00:00:00.000000000 Z
12
+ date: 2024-12-09 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: anyway_config
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: dry-initializer
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -98,11 +112,14 @@ files:
98
112
  - README.md
99
113
  - Rakefile
100
114
  - app/assets/javascripts/tramway/multiselect_controller.js
115
+ - app/assets/javascripts/tramway/table_row_preview_controller.js
101
116
  - app/components/tailwind_component.html.haml
102
117
  - app/components/tailwind_component.rb
103
118
  - app/components/tailwinds/form/builder.rb
104
119
  - app/components/tailwinds/form/file_field_component.html.haml
105
120
  - app/components/tailwinds/form/file_field_component.rb
121
+ - app/components/tailwinds/form/label_component.html.haml
122
+ - app/components/tailwinds/form/label_component.rb
106
123
  - app/components/tailwinds/form/multiselect/dropdown_container.html.haml
107
124
  - app/components/tailwinds/form/multiselect/dropdown_container.rb
108
125
  - app/components/tailwinds/form/multiselect/item_container.html.haml
@@ -125,6 +142,32 @@ files:
125
142
  - app/components/tailwinds/nav/item/link_component.rb
126
143
  - app/components/tailwinds/navbar_component.html.haml
127
144
  - app/components/tailwinds/navbar_component.rb
145
+ - app/components/tailwinds/pagination/base.rb
146
+ - app/components/tailwinds/pagination/first_page_component.html.haml
147
+ - app/components/tailwinds/pagination/first_page_component.rb
148
+ - app/components/tailwinds/pagination/gap_component.html.haml
149
+ - app/components/tailwinds/pagination/gap_component.rb
150
+ - app/components/tailwinds/pagination/last_page_component.html.haml
151
+ - app/components/tailwinds/pagination/last_page_component.rb
152
+ - app/components/tailwinds/pagination/next_page_component.html.haml
153
+ - app/components/tailwinds/pagination/next_page_component.rb
154
+ - app/components/tailwinds/pagination/page_component.html.haml
155
+ - app/components/tailwinds/pagination/page_component.rb
156
+ - app/components/tailwinds/pagination/prev_page_component.html.haml
157
+ - app/components/tailwinds/pagination/prev_page_component.rb
158
+ - app/components/tailwinds/table/cell_component.html.haml
159
+ - app/components/tailwinds/table/cell_component.rb
160
+ - app/components/tailwinds/table/header_component.html.haml
161
+ - app/components/tailwinds/table/header_component.rb
162
+ - app/components/tailwinds/table/row/preview_component.html.haml
163
+ - app/components/tailwinds/table/row/preview_component.rb
164
+ - app/components/tailwinds/table/row_component.html.haml
165
+ - app/components/tailwinds/table/row_component.rb
166
+ - app/components/tailwinds/table_component.html.haml
167
+ - app/components/tailwinds/table_component.rb
168
+ - app/components/tramway/component/base.rb
169
+ - app/controllers/tramway/entities_controller.rb
170
+ - app/helpers/tramway/application_helper.rb
128
171
  - app/views/kaminari/_first_page.html.haml
129
172
  - app/views/kaminari/_gap.html.haml
130
173
  - app/views/kaminari/_last_page.html.haml
@@ -132,6 +175,8 @@ files:
132
175
  - app/views/kaminari/_page.html.haml
133
176
  - app/views/kaminari/_paginator.html.haml
134
177
  - app/views/kaminari/_prev_page.html.haml
178
+ - app/views/tramway/entities/_entity.html.haml
179
+ - app/views/tramway/entities/index.html.haml
135
180
  - config/routes.rb
136
181
  - lib/rules/turbo_html_attributes_rules.rb
137
182
  - lib/tasks/tramway_tasks.rake
@@ -151,6 +196,7 @@ files:
151
196
  - lib/tramway/forms/class_helper.rb
152
197
  - lib/tramway/forms/normalizations.rb
153
198
  - lib/tramway/forms/properties.rb
199
+ - lib/tramway/helpers/component_helper.rb
154
200
  - lib/tramway/helpers/decorate_helper.rb
155
201
  - lib/tramway/helpers/form_helper.rb
156
202
  - lib/tramway/helpers/navbar_helper.rb