tramway 0.4.9.3 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
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