tramway 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c116b7002a5f382a5b2d54704e425d6a7e2e1a5208cb2eb1f0cd060bf5852718
4
- data.tar.gz: 6a183fa879258d651afcd2479265854139dcc8e5da69f9684a331f859274de64
3
+ metadata.gz: c3edafce1c744c1e57499e5682ee75f749dbf5bdefd1a9137d70ccaa95f1381f
4
+ data.tar.gz: fbb70ec105b76caad025797238eb09a7e21d5892e90be6b77c1c2858f301c666
5
5
  SHA512:
6
- metadata.gz: 7765e1c762c50ba2cb543ef79f155f6649cd5c5ac0ee38aa530f9472f0a7024ca489473ac0cf597a30a8fa2c99ce2ed8fde0d62fa34198a7903e3c90e8edfc92
7
- data.tar.gz: d136698275286b8188609c2883756a9e338ca6c6faf118166f1374f443ae68ac7ed99849811703f6bf2f70e71d5aaadaec1433d4bec41e70568e27fea015edf1
6
+ metadata.gz: 4deef971e492a823f04475bc6e915193735844521f8ae65ba8f07da1b1b0edb40e35d944000b648e0c36f1e33d8b2e770b6b62117f52df1a81443fb034132032
7
+ data.tar.gz: fcbea51c9f88495f24110ccac4f473a3a76f2583621fb952ec849fc77c690ee081bcba7a4e789c04f1c77bda02c41dac2d607f7c9b5a6572cdd03172cf31ac0c
data/README.md CHANGED
@@ -8,6 +8,7 @@ Unite Ruby on Rails brilliance. Streamline development with Tramway.
8
8
  * [Tramway Decorators](https://github.com/Purple-Magic/tramway#tramway-decorators)
9
9
  * [Tramway Form](https://github.com/Purple-Magic/tramway#tramway-form)
10
10
  * [Tramway Navbar](https://github.com/Purple-Magic/tramway#tramway-navbar)
11
+ * [Tramway Table Component](https://github.com/Purple-Magic/tramway#tramway-table-component)
11
12
  * [Tailwind-styled forms](https://github.com/Purple-Magic/tramway#tailwind-styled-forms)
12
13
  * [Stimulus-based inputs](https://github.com/Purple-Magic/tramway#stimulus-based-inputs)
13
14
  * [Tailwind-styled pagination](https://github.com/Purple-Magic/tramway?tab=readme-ov-file#tailwind-styled-pagination-for-kaminari)
@@ -194,6 +195,8 @@ end
194
195
 
195
196
  #### Decorate associations
196
197
 
198
+ **Decorate single association**
199
+
197
200
  ```ruby
198
201
  class UserDecorator < Tramway::BaseDecorator
199
202
  association :posts
@@ -203,6 +206,14 @@ user = tramway_decorate User.first
203
206
  user.posts # => decorated collection of posts with PostDecorator
204
207
  ```
205
208
 
209
+ **Decorate multiple associations**
210
+
211
+ ```ruby
212
+ class UserDecorator < Tramway::BaseDecorator
213
+ associations :posts, :users
214
+ end
215
+ ```
216
+
206
217
  #### Decorate nil
207
218
 
208
219
  Tramway Decorator does not decorate nil objects
@@ -491,6 +502,20 @@ tramway_navbar title: 'Purple Magic' do |nav|
491
502
  end
492
503
  ```
493
504
 
505
+ ### Tramway Table Component
506
+
507
+ Tramway provides a responsive, tailwind-styled table with light and dark themes.
508
+
509
+ ```haml
510
+ = component 'tailwinds/table' do
511
+ = component 'tailwinds/table/header', headers: ['Column 1', 'Column 2']
512
+ = component 'tailwinds/table/row' do
513
+ = component 'tailwinds/table/cell' do
514
+ Something
515
+ = component 'tailwinds/table/cell' do
516
+ Another
517
+ ```
518
+
494
519
  ### Tailwind-styled forms
495
520
 
496
521
  Tramway uses [Tailwind](https://tailwindcss.com/) by default. All UI helpers are implemented with [ViewComponent](https://github.com/viewcomponent/view_component).
@@ -2,4 +2,4 @@
2
2
  - if @label
3
3
  = component('tailwinds/form/label', for: @for) do
4
4
  = @label
5
- = @input.call(@attribute, @collection, { selected: @value }, @options.merge(class: 'bg-white border border-gray-300 text-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent py-2 px-3'))
5
+ = @input.call(@attribute, @collection, { selected: @value }, @options.merge(class: 'bg-white border border-gray-300 text-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent py-2 px-3 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed'))
@@ -1,4 +1,4 @@
1
- .div-table-row.block.grid.text-white.text-small.gap-4.bg-purple-700.dark:bg-gray-700.dark:text-gray-400{ class: "grid-cols-#{headers.count}" }
1
+ .div-table-row.grid.text-white.text-small.gap-4.bg-purple-700.dark:bg-gray-700.dark:text-gray-400{ class: "grid-cols-#{headers.count}", aria: { label: "Table Header" }, role: "row" }
2
2
  - if headers.any?
3
3
  - headers.each do |header|
4
4
  .div-table-cell.py-4.px-6
@@ -9,14 +9,16 @@ module Tailwinds
9
9
  option :options, optional: true, default: -> { {} }
10
10
 
11
11
  def row_tag(**options, &)
12
+ default_attributes = { role: :row }
13
+
12
14
  if href.present?
13
15
  klass = "#{options[:class] || ''} #{link_row_classes}"
14
16
 
15
- link_to(href, options.merge(class: klass)) do
17
+ link_to(href, options.merge(class: klass, **default_attributes)) do
16
18
  yield if block_given?
17
19
  end
18
20
  else
19
- tag.div(**options) do
21
+ tag.div(**options.merge(default_attributes)) do
20
22
  yield if block_given?
21
23
  end
22
24
  end
@@ -1,4 +1,5 @@
1
1
  = helpers.component 'tailwinds/table/row/preview'
2
2
 
3
- .div-table.w-full.text-left.rtl:text-right.text-gray-500.dark:text-gray-400.mt-4{ **options }
3
+ - width_class = options[:class]&.include?('w-') ? '' : 'w-full'
4
+ .div-table.text-left.rtl:text-right.text-gray-500.dark:text-gray-400.mt-4{ class: "#{options[:class]} #{width_class}", **options.except(:class) }
4
5
  = content
@@ -11,7 +11,12 @@ module Tramway
11
11
  include Rails.application.routes.url_helpers
12
12
 
13
13
  def index
14
- @entities = model_class.order(id: :desc).page(params[:page])
14
+ @entities = if entity.page(:index).scope.present?
15
+ model_class.public_send(entity.page(:index).scope)
16
+ else
17
+ model_class.order(id: :desc)
18
+ end.page(params[:page])
19
+ @namespace = entity.route.namespace
15
20
  end
16
21
 
17
22
  private
@@ -19,5 +24,9 @@ module Tramway
19
24
  def model_class
20
25
  @model_class ||= params[:entity][:name].classify.constantize
21
26
  end
27
+
28
+ def entity
29
+ @entity ||= Tramway.config.entities.find { |e| e.name == params[:entity][:name] }
30
+ end
22
31
  end
23
32
  end
@@ -1,3 +1,3 @@
1
- - decorator = decorator_class(entity)
2
- - decorated_object = decorator.decorate(entity)
3
- = component 'tailwinds/table/row', cells: decorator.list_attributes.reduce({}) { |hash, attribute| hash.merge! attribute => decorated_object.public_send(attribute) }, href: decorated_object.show_path
1
+ - decorator = decorator_class_name(entity, @namespace)
2
+ - decorated_object = decorator.constantize.decorate(entity)
3
+ = component 'tailwinds/table/row', cells: decorator.constantize.list_attributes.reduce({}) { |hash, attribute| hash.merge! attribute => decorated_object.public_send(attribute) }, href: decorated_object.show_path
@@ -1,4 +1,4 @@
1
- - decorator = Tramway::Decorators::NameBuilder.default_decorator_class_name(@model_class)
1
+ - decorator = Tramway::Decorators::ClassHelper.decorator_class_name(@model_class, @namespace)
2
2
  - list_attributes = decorator.constantize.list_attributes
3
3
 
4
4
  .mt-8.w-full
data/config/routes.rb CHANGED
@@ -2,29 +2,30 @@
2
2
 
3
3
  Tramway::Engine.routes.draw do
4
4
  Tramway.config.entities.each do |entity|
5
- define_resource = lambda do |resource_name, entity| # rubocop:disable Lint/ShadowingOuterLocalVariable
5
+ segments = entity.name.split('/')
6
+ resource_name = segments.pop
7
+
8
+ define_resource = proc do
6
9
  resources resource_name.pluralize.to_sym,
7
- only: [:index],
8
- controller: '/tramway/entities',
9
- defaults: { entity: }
10
+ only: [:index],
11
+ controller:'/tramway/entities',
12
+ defaults: { entity: entity }
10
13
  end
11
14
 
12
- if entity.name.include?('/')
13
- *namespaces, resource_name = entity.name.split('/')
14
-
15
- define_namespace = lambda do |index|
16
- namespace namespaces[index].to_sym do
17
- if namespaces[index] == namespaces.last
18
- define_resource.call(resource_name, entity)
15
+ if segments.empty?
16
+ define_resource.call
17
+ else
18
+ nest = lambda do |names|
19
+ namespace names.first.to_sym do
20
+ if names.size > 1
21
+ nest.call(names.drop(1))
19
22
  else
20
- define_namespace.call(namespaces[index + 1])
23
+ define_resource.call
21
24
  end
22
25
  end
23
26
  end
24
27
 
25
- define_namespace.call(0)
26
- else
27
- define_resource.call(entity.name, entity)
28
+ nest.call(segments)
28
29
  end
29
30
  end
30
31
  end
@@ -50,6 +50,9 @@ module.exports = {
50
50
  'space-x-1',
51
51
  'justify-end',
52
52
  'mt-2',
53
+ 'disabled:bg-gray-100',
54
+ 'disabled:text-gray-400',
55
+ 'disabled:cursor-not-allowed',
53
56
  // pagination
54
57
  'bg-white', 'rounded-md', 'hover:bg-purple-100', 'dark:text-white', 'dark:bg-gray-800', 'dark:hover:bg-gray-700',
55
58
  // multiselect styles
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Configs
5
+ module Entities
6
+ # Route struct describes rules for route management
7
+ #
8
+ class Page < Dry::Struct
9
+ attribute :action, Types::Coercible::String
10
+ attribute? :scope, Types::Coercible::String
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'tramway/configs/entities/route'
4
+ require 'tramway/configs/entities/page'
4
5
 
5
6
  module Tramway
6
7
  module Configs
7
8
  # Tramway is an entity-based framework
8
9
  class Entity < Dry::Struct
9
10
  attribute :name, Types::Coercible::String
10
- attribute? :pages, Types::Array.of(Types::Symbol).default([].freeze)
11
+ attribute? :pages, Types::Array.of(Tramway::Configs::Entities::Page).default([].freeze)
11
12
  attribute? :route, Tramway::Configs::Entities::Route
12
13
 
13
14
  # Route Struct contains implemented in Tramway CRUD and helpful routes for the entity
14
15
  RouteStruct = Struct.new(:index)
15
16
 
16
- # HumanNameStruct contains human names forms for the entity
17
+ # HumanName Struct contains human names forms for the entity
17
18
  HumanNameStruct = Struct.new(:single, :plural)
18
19
 
19
20
  def routes
@@ -31,6 +32,10 @@ module Tramway
31
32
  HumanNameStruct.new(single, plural)
32
33
  end
33
34
 
35
+ def page(name)
36
+ pages.find { |page| page.action == name.to_s }
37
+ end
38
+
34
39
  private
35
40
 
36
41
  def pluralized(model_name)
@@ -48,7 +53,7 @@ module Tramway
48
53
  def route_helper_method
49
54
  underscored_name = name.parameterize.pluralize.underscore
50
55
 
51
- method_name = if pages.include?(:index) || route.blank?
56
+ method_name = if set_page?(:index) || route.blank?
52
57
  "#{underscored_name}_path"
53
58
  else
54
59
  route.helper_method_by(underscored_name)
@@ -58,8 +63,10 @@ module Tramway
58
63
  end
59
64
 
60
65
  def route_helper_engine
61
- pages.include?(:index) ? Tramway::Engine : Rails.application
66
+ set_page?(:index) ? Tramway::Engine : Rails.application
62
67
  end
68
+
69
+ alias set_page? page
63
70
  end
64
71
  end
65
72
  end
@@ -22,14 +22,14 @@ module Tramway
22
22
  end
23
23
 
24
24
  # has_and_belongs_to_many is not supported for now
25
- def association(association)
25
+ def association(association, decorator: nil)
26
26
  define_method(association) do
27
27
  assoc = object.send(association)
28
28
 
29
29
  if assoc.is_a?(ActiveRecord::Relation)
30
- AssocDecoratorHelper.decorate_has_many_association assoc
30
+ AssocDecoratorHelper.decorate_has_many_association assoc, decorator_class: decorator
31
31
  elsif assoc.present?
32
- AssocDecoratorHelper.decorate_associated_object(assoc)
32
+ AssocDecoratorHelper.decorate_associated_object(assoc, decorator_class: decorator)
33
33
  end
34
34
  end
35
35
  end
@@ -38,12 +38,18 @@ module Tramway
38
38
  # Helper module for association decorators
39
39
  module AssocDecoratorHelper
40
40
  class << self
41
- def decorate_has_many_association(assoc)
42
- assoc.empty? ? [] : decorator(assoc.klass).decorate(assoc)
41
+ def decorate_has_many_association(assoc, decorator_class: nil)
42
+ return [] if assoc.empty?
43
+
44
+ decorator_class ||= decorator(assoc.klass)
45
+
46
+ decorator_class.decorate(assoc)
43
47
  end
44
48
 
45
- def decorate_associated_object(assoc)
46
- decorator(assoc.class).decorate(assoc)
49
+ def decorate_associated_object(assoc, decorator_class: nil)
50
+ decorator_class ||= decorator(assoc.class)
51
+
52
+ decorator_class.decorate(assoc)
47
53
  end
48
54
 
49
55
  def decorator(class_name)
@@ -6,27 +6,31 @@ module Tramway
6
6
  module ClassHelper
7
7
  module_function
8
8
 
9
- def decorator_class(object_or_array, decorator = nil)
9
+ def decorator_class(object_or_array, decorator = nil, namespace = nil)
10
10
  raise_error_if_object_empty object_or_array, decorator
11
11
 
12
12
  return decorator if decorator.present?
13
13
 
14
14
  begin
15
- class_name = decorator_class_name(object_or_array)
15
+ class_name = decorator_class_name(object_or_array, namespace)
16
16
  class_name.constantize
17
17
  rescue NameError
18
18
  raise NameError, "You should define #{class_name} decorator class."
19
19
  end
20
20
  end
21
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
22
+ def decorator_class_name(object_or_array_or_class, namespace)
23
+ klass = if Tramway::Decorators::CollectionDecorators.collection?(object_or_array_or_class)
24
+ object_or_array_or_class.first.class
25
+ elsif object_or_array_or_class.is_a?(Class)
26
+ object_or_array_or_class
25
27
  else
26
- object_or_array.class
28
+ object_or_array_or_class.class
27
29
  end
28
30
 
29
- Tramway::Decorators::NameBuilder.default_decorator_class_name(klass)
31
+ base_class_name = Tramway::Decorators::NameBuilder.default_decorator_class_name(klass)
32
+
33
+ namespace.present? ? "#{namespace.to_s.camelize}::#{base_class_name}" : base_class_name
30
34
  end
31
35
 
32
36
  # :reek:NilCheck { enabled: false }
@@ -9,7 +9,23 @@ module Tramway
9
9
  def property(attribute)
10
10
  @properties << attribute
11
11
 
12
- delegate attribute, to: :object
12
+ define_method(attribute) do
13
+ if object.respond_to?(attribute)
14
+ object.public_send(attribute)
15
+ else
16
+ raise NoMethodError, "#{self.class}##{attribute} is not defined"
17
+ end
18
+ end
19
+
20
+ set_method = "#{attribute}="
21
+
22
+ define_method(set_method) do |value|
23
+ if object.respond_to?(set_method)
24
+ object.public_send(set_method, value)
25
+ else
26
+ raise NoMethodError, "#{self.class}##{set_method} is not defined"
27
+ end
28
+ end
13
29
  end
14
30
 
15
31
  def properties(*attributes)
@@ -8,12 +8,12 @@ module Tramway
8
8
  #
9
9
  module DecorateHelper
10
10
  # :reek:NilCheck { enabled: false } because checking for nil is not a type-checking issue but business logic
11
- def tramway_decorate(object_or_array, decorator: nil)
11
+ def tramway_decorate(object_or_array, decorator: nil, namespace: nil)
12
12
  return [] if Tramway::Decorators::CollectionDecorators.collection?(object_or_array) && object_or_array.empty?
13
13
 
14
14
  return if object_or_array.nil?
15
15
 
16
- Tramway::Decorators::ClassHelper.decorator_class(object_or_array, decorator).decorate object_or_array
16
+ Tramway::Decorators::ClassHelper.decorator_class(object_or_array, decorator, namespace).decorate object_or_array
17
17
  end
18
18
  end
19
19
  end
@@ -6,8 +6,8 @@ module Tramway
6
6
  module Helpers
7
7
  # Provides navbar helpers for ActionView
8
8
  module NavbarHelper
9
- def tramway_navbar(**options)
10
- initialize_navbar
9
+ def tramway_navbar(with_entities: true, **options)
10
+ initialize_navbar(with_entities:)
11
11
 
12
12
  yield @navbar if block_given?
13
13
 
@@ -18,8 +18,8 @@ module Tramway
18
18
 
19
19
  private
20
20
 
21
- def initialize_navbar
22
- @navbar = Tramway::Navbar.new self
21
+ def initialize_navbar(with_entities:)
22
+ @navbar = Tramway::Navbar.new self, with_entities:
23
23
  end
24
24
 
25
25
  def assign_navbar_items(options)
@@ -5,16 +5,18 @@ module Tramway
5
5
  class Navbar
6
6
  attr_reader :items, :context
7
7
 
8
- def initialize(context)
8
+ def initialize(context, with_entities:)
9
9
  @context = context
10
10
  @items = { left: [], right: [] }
11
11
  @filling = nil
12
12
 
13
- entities = Tramway.config.entities
13
+ if with_entities
14
+ entities = Tramway.config.entities
14
15
 
15
- return unless entities.any?
16
+ return unless entities.any?
16
17
 
17
- preset_left entities
18
+ preset_left entities
19
+ end
18
20
  end
19
21
 
20
22
  def left
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tramway
4
- VERSION = '0.5.3'
4
+ VERSION = '0.5.4'
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.5.3
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - kalashnikovisme
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-03-19 00:00:00.000000000 Z
12
+ date: 2025-06-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: anyway_config
@@ -219,6 +219,7 @@ files:
219
219
  - lib/tramway/base_decorator.rb
220
220
  - lib/tramway/base_form.rb
221
221
  - lib/tramway/config.rb
222
+ - lib/tramway/configs/entities/page.rb
222
223
  - lib/tramway/configs/entities/route.rb
223
224
  - lib/tramway/configs/entity.rb
224
225
  - lib/tramway/decorators/association.rb