phlexi-table 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/phlexi/table/base.rb +343 -74
  3. data/lib/phlexi/table/components/actions_column.rb +49 -0
  4. data/lib/phlexi/table/components/base.rb +23 -18
  5. data/lib/phlexi/table/components/column.rb +24 -0
  6. data/lib/phlexi/table/components/column_group.rb +23 -0
  7. data/lib/phlexi/table/components/concerns/displays_data.rb +66 -0
  8. data/lib/phlexi/table/components/concerns/displays_header.rb +114 -0
  9. data/lib/phlexi/table/components/concerns/groups_columns.rb +36 -0
  10. data/lib/phlexi/table/components/options/alignment.rb +31 -0
  11. data/lib/phlexi/table/components/options/associations.rb +23 -0
  12. data/lib/phlexi/table/components/options/attachments.rb +23 -0
  13. data/lib/phlexi/table/components/options/labels.rb +30 -0
  14. data/lib/phlexi/table/components/options/types.rb +140 -0
  15. data/lib/phlexi/table/html.rb +13 -0
  16. data/lib/phlexi/table/table_options/captions.rb +22 -0
  17. data/lib/phlexi/table/{field_options/description.rb → table_options/descriptions.rb} +5 -5
  18. data/lib/phlexi/table/theme.rb +29 -0
  19. data/lib/phlexi/table/version.rb +1 -1
  20. data/lib/phlexi/table/wrapped_object.rb +20 -0
  21. data/lib/phlexi/table.rb +3 -5
  22. metadata +32 -25
  23. data/lib/phlexi/table/components/concerns/displays_value.rb +0 -54
  24. data/lib/phlexi/table/components/date_time.rb +0 -49
  25. data/lib/phlexi/table/components/description.rb +0 -21
  26. data/lib/phlexi/table/components/hint.rb +0 -21
  27. data/lib/phlexi/table/components/label.rb +0 -15
  28. data/lib/phlexi/table/components/number.rb +0 -37
  29. data/lib/phlexi/table/components/placeholder.rb +0 -15
  30. data/lib/phlexi/table/components/string.rb +0 -17
  31. data/lib/phlexi/table/components/wrapper.rb +0 -17
  32. data/lib/phlexi/table/field_options/associations.rb +0 -21
  33. data/lib/phlexi/table/field_options/attachments.rb +0 -21
  34. data/lib/phlexi/table/field_options/hints.rb +0 -22
  35. data/lib/phlexi/table/field_options/inferred_types.rb +0 -129
  36. data/lib/phlexi/table/field_options/labels.rb +0 -28
  37. data/lib/phlexi/table/field_options/placeholders.rb +0 -18
  38. data/lib/phlexi/table/field_options/themes.rb +0 -132
  39. data/lib/phlexi/table/structure/dom.rb +0 -42
  40. data/lib/phlexi/table/structure/field_builder.rb +0 -158
  41. data/lib/phlexi/table/structure/field_collection.rb +0 -39
  42. data/lib/phlexi/table/structure/namespace.rb +0 -123
  43. data/lib/phlexi/table/structure/namespace_collection.rb +0 -40
  44. data/lib/phlexi/table/structure/node.rb +0 -24
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Components
6
+ module Concerns
7
+ module DisplaysData
8
+ extend ActiveSupport::Concern
9
+
10
+ class DataCell < Phlexi::Table::HTML
11
+ def initialize(value)
12
+ @value = value
13
+ end
14
+
15
+ def view_template
16
+ plain @value.to_s
17
+ end
18
+ end
19
+
20
+ class SelectionCell < Phlexi::Table::HTML
21
+ def initialize(value)
22
+ @value = value
23
+ end
24
+
25
+ def view_template
26
+ input(type: :checkbox, value: @value, class: themed(:selection_checkbox))
27
+ end
28
+ end
29
+
30
+ def data_cell(object)
31
+ case type
32
+ when :selection
33
+ selection_cell_class.new(object.value_of(key))
34
+ else
35
+ data_cell_class.new(object.value_of(key))
36
+ end
37
+ end
38
+
39
+ def data_cell_attributes(object)
40
+ attributes = @attributes.dup
41
+ case type
42
+ when :name, :selection
43
+ attributes["scope"] = "row"
44
+ end
45
+ attributes[:class] = tokens(
46
+ attributes[:class],
47
+ themed(:"align_#{alignment}"),
48
+ themed(:"#{type}_column")
49
+ )
50
+ attributes
51
+ end
52
+
53
+ private
54
+
55
+ def selection_cell_class
56
+ self.class::SelectionCell
57
+ end
58
+
59
+ def data_cell_class
60
+ self.class::DataCell
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Components
6
+ module Concerns
7
+ module DisplaysHeader
8
+ extend ActiveSupport::Concern
9
+
10
+ class HeaderCell < Phlexi::Table::HTML
11
+ def initialize(value, sort_params:)
12
+ @value = value
13
+ @sort_params = sort_params
14
+ end
15
+
16
+ def view_template
17
+ div(class: "flex items-center") do
18
+ if !@sort_params
19
+ plain @value.to_s
20
+ next
21
+ end
22
+
23
+ a(class: "flex items-center", href: @sort_params[:url]) do
24
+ plain @value.to_s
25
+
26
+ sort_icon = Phlexi::Table::Theme.instance.resolve_theme(:sort_icon)
27
+ sort_icon_active = Phlexi::Table::Theme.instance.resolve_theme(:sort_icon_active)
28
+
29
+ span(class: "ml-1.5") {
30
+ svg(
31
+ class: tokens(sort_icon, -> { @sort_params[:direction] != "DESC" } => sort_icon_active),
32
+ aria_hidden: "true",
33
+ xmlns: "http://www.w3.org/2000/svg",
34
+ fill: "none",
35
+ viewbox: "0 0 10 6"
36
+ ) do |s|
37
+ s.path(
38
+ stroke: "currentColor",
39
+ stroke_linecap: "round",
40
+ stroke_linejoin: "round",
41
+ stroke_width: "2",
42
+ d: "M9 5 5 1 1 5"
43
+ )
44
+ end
45
+ svg(
46
+ class: tokens(sort_icon, -> { @sort_params[:direction] != "ASC" } => sort_icon_active),
47
+ aria_hidden: "true",
48
+ xmlns: "http://www.w3.org/2000/svg",
49
+ fill: "none",
50
+ viewbox: "0 0 10 6"
51
+ ) do |s|
52
+ s.path(
53
+ stroke: "currentColor",
54
+ stroke_linecap: "round",
55
+ stroke_linejoin: "round",
56
+ stroke_width: "2",
57
+ d: "M1 1 5 5 9 1"
58
+ )
59
+ end
60
+ }
61
+ end
62
+
63
+ next if @sort_params[:position].nil?
64
+
65
+ sort_index_clear_link = Phlexi::Table::Theme.instance.resolve_theme(:sort_index_clear_link)
66
+ sort_index_clear_link_text = Phlexi::Table::Theme.instance.resolve_theme(:sort_index_clear_link_text)
67
+ sort_index_clear_link_icon = Phlexi::Table::Theme.instance.resolve_theme(:sort_index_clear_link_icon)
68
+
69
+ a(href: @sort_params[:reset_url], title: "Clear sort", class: sort_index_clear_link) {
70
+ span(class: sort_index_clear_link_text) { (@sort_params[:position] + 1).to_s }
71
+ span(class: sort_index_clear_link_icon) { "✕" }
72
+ }
73
+ end
74
+ end
75
+ end
76
+
77
+ def header_cell
78
+ case type
79
+ when :selection
80
+ selection_cell_class.new(nil)
81
+ else
82
+ header_cell_class.new(label, sort_params:)
83
+ end
84
+ end
85
+
86
+ def header_cell_attributes
87
+ attributes = @attributes.dup
88
+ attributes[:id] = "#{dom_id}_header_cell"
89
+ attributes[:class] = tokens(
90
+ attributes[:class],
91
+ themed(:"align_#{alignment}"),
92
+ themed(:"#{type}_column")
93
+ )
94
+ attributes
95
+ end
96
+
97
+ def colspan = 1
98
+
99
+ protected
100
+
101
+ def sort_params
102
+ options[:sort_params]
103
+ end
104
+
105
+ private
106
+
107
+ def header_cell_class
108
+ self.class::HeaderCell
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Components
6
+ module Concerns
7
+ module GroupsColumns
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ attr_reader :columns
12
+ end
13
+
14
+ def initialize(*, **, &)
15
+ super(*, **)
16
+
17
+ @columns = {}
18
+ yield self if block_given?
19
+ end
20
+
21
+ def column(key, **, &)
22
+ add_column(parent.column_class.new(key, self, **, &))
23
+ end
24
+
25
+ private
26
+
27
+ def add_column(column)
28
+ raise ArgumentError, "Column '#{column.key}' already exists" if @columns.key?(column.key) || parent.columns.key?(column.key)
29
+
30
+ @columns[column.key] = column
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Components
6
+ module Options
7
+ module Alignment
8
+ def alignment(alignment = nil)
9
+ if alignment.nil?
10
+ options[:alignment] = options.fetch(:alignment) { calculate_alignment }
11
+ else
12
+ options[:alignment] = alignment
13
+ self
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def calculate_alignment
20
+ case type
21
+ when :integer, :float, :decimal
22
+ :end
23
+ else
24
+ :start
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Components
6
+ module Options
7
+ module Associations
8
+ protected
9
+
10
+ def association_reflection
11
+ @association_reflection ||= find_association_reflection
12
+ end
13
+
14
+ def find_association_reflection
15
+ if sample.class.respond_to?(:reflect_on_association)
16
+ sample.class.reflect_on_association(key)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Components
6
+ module Options
7
+ module Attachments
8
+ protected
9
+
10
+ def attachment_reflection
11
+ @attachment_reflection ||= find_attachment_reflection
12
+ end
13
+
14
+ def find_attachment_reflection
15
+ if sample.class.respond_to?(:reflect_on_attachment)
16
+ sample.class.reflect_on_attachment(key)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module Components
6
+ module Options
7
+ module Labels
8
+ def label(label = nil)
9
+ if label.nil?
10
+ options[:label] = options.fetch(:label) { calculate_label }
11
+ else
12
+ options[:label] = label
13
+ self
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def calculate_label
20
+ if sample.class.respond_to?(:human_attribute_name)
21
+ sample.class.human_attribute_name(key.to_s, {base: sample})
22
+ else
23
+ key.to_s.humanize
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module Phlexi
6
+ module Table
7
+ module Components
8
+ module Options
9
+ module Types
10
+ def type(type = nil)
11
+ if type.nil?
12
+ options[:as] = options.fetch(:as) { inferred_db_type }
13
+ else
14
+ options[:as] = type
15
+ self
16
+ end
17
+ end
18
+
19
+ def inferred_db_type
20
+ @inferred_db_type ||= infer_db_type
21
+ end
22
+
23
+ def inferred_display_component
24
+ @inferred_display_component ||= infer_display_component
25
+ end
26
+
27
+ private
28
+
29
+ def infer_display_component
30
+ case type
31
+ when :string, :text
32
+ infer_string_display_type(key)
33
+ when :integer, :float, :decimal
34
+ :number
35
+ when :date, :datetime, :time
36
+ :date
37
+ when :boolean
38
+ :boolean
39
+ when :json, :jsonb, :hstore
40
+ :code
41
+ else
42
+ if association_reflection
43
+ :association
44
+ elsif attachment_reflection
45
+ :attachment
46
+ else
47
+ :text
48
+ end
49
+ end
50
+ end
51
+
52
+ def infer_db_type
53
+ if sample.class.respond_to?(:columns_hash)
54
+ # ActiveRecord model
55
+ column = sample.class.columns_hash[key.to_s]
56
+ return column.type if column
57
+ end
58
+
59
+ if sample.class.respond_to?(:attribute_types)
60
+ # ActiveModel::Attributes
61
+ custom_type = sample.class.attribute_types[key.to_s]
62
+ return custom_type.type if custom_type&.type
63
+ end
64
+
65
+ # Check if sample responds to the key
66
+ if sample.respond_to?(key)
67
+ # Fallback to inferring type from the value
68
+ return infer_db_type_from_value(sample.send(key))
69
+ end
70
+
71
+ # Default to string if we can't determine the type
72
+ :string
73
+ end
74
+
75
+ def infer_db_type_from_value(value)
76
+ case value
77
+ when Integer
78
+ :integer
79
+ when Float
80
+ :float
81
+ when BigDecimal
82
+ :decimal
83
+ when TrueClass, FalseClass
84
+ :boolean
85
+ when Date
86
+ :date
87
+ when Time, DateTime
88
+ :datetime
89
+ when Hash
90
+ :json
91
+ else
92
+ :string
93
+ end
94
+ end
95
+
96
+ def infer_string_display_type(key)
97
+ key = key.to_s.downcase
98
+
99
+ return :password if is_password_field?
100
+
101
+ custom_type = custom_string_display_type(key)
102
+ return custom_type if custom_type
103
+
104
+ :text
105
+ end
106
+
107
+ def custom_string_display_type(key)
108
+ custom_mappings = {
109
+ /url$|^link|^site/ => :url,
110
+ /^email/ => :email,
111
+ /phone|tel(ephone)?/ => :phone,
112
+ /^time/ => :time,
113
+ /^date/ => :date,
114
+ /^number|_count$|_amount$/ => :number,
115
+ /^color/ => :color
116
+ }
117
+
118
+ custom_mappings.each do |pattern, type|
119
+ return type if key.match?(pattern)
120
+ end
121
+
122
+ nil
123
+ end
124
+
125
+ def is_password_field?
126
+ key = self.key.to_s.downcase
127
+
128
+ exact_matches = ["password"]
129
+ prefixes = ["encrypted_"]
130
+ suffixes = ["_password", "_digest", "_hash"]
131
+
132
+ exact_matches.include?(key) ||
133
+ prefixes.any? { |prefix| key.start_with?(prefix) } ||
134
+ suffixes.any? { |suffix| key.end_with?(suffix) }
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,13 @@
1
+ require "phlex"
2
+
3
+ module Phlexi
4
+ module Table
5
+ class HTML < (defined?(::ApplicationComponent) ? ::ApplicationComponent : Phlex::HTML)
6
+ protected
7
+
8
+ def themed(component)
9
+ Phlexi::Table::Theme.instance.resolve_theme(component)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlexi
4
+ module Table
5
+ module TableOptions
6
+ module Captions
7
+ def table_caption(caption = nil)
8
+ if caption.nil?
9
+ options[:caption]
10
+ else
11
+ options[:caption] = caption
12
+ self
13
+ end
14
+ end
15
+
16
+ def has_table_caption?
17
+ table_caption.present?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Phlexi
4
4
  module Table
5
- module FieldOptions
6
- module Description
7
- def description(description = nil)
5
+ module TableOptions
6
+ module Descriptions
7
+ def table_description(description = nil)
8
8
  if description.nil?
9
9
  options[:description]
10
10
  else
@@ -13,8 +13,8 @@ module Phlexi
13
13
  end
14
14
  end
15
15
 
16
- def has_description?
17
- description.present?
16
+ def has_table_description?
17
+ table_description.present?
18
18
  end
19
19
  end
20
20
  end
@@ -0,0 +1,29 @@
1
+ module Phlexi
2
+ module Table
3
+ class Theme
4
+ include Phlexi::Field::Theme
5
+
6
+ DEFAULT_THEME = {
7
+ wrapper: nil,
8
+ base: nil,
9
+ caption: nil,
10
+ description: nil,
11
+ header: nil,
12
+ header_grouping_row: :header_row,
13
+ header_grouping_cell: :header_row_cell,
14
+ header_row: nil,
15
+ header_cell: nil,
16
+ body_row: nil,
17
+ body_cell: nil,
18
+ name_column: nil,
19
+ align_end: nil,
20
+ selection_checkbox: nil,
21
+ actions_row_cell: nil
22
+ }.freeze
23
+
24
+ def theme
25
+ DEFAULT_THEME
26
+ end
27
+ end
28
+ end
29
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlexi
4
4
  module Table
5
- VERSION = "0.0.1"
5
+ VERSION = "0.0.2"
6
6
  end
7
7
  end
@@ -0,0 +1,20 @@
1
+ module Phlexi
2
+ module Table
3
+ class WrappedObject
4
+ attr_reader :unwrapped
5
+
6
+ def initialize(object, index:)
7
+ @unwrapped = object
8
+ @index = index
9
+ end
10
+
11
+ def id
12
+ value_of(:id) || index
13
+ end
14
+
15
+ def value_of(key)
16
+ @unwrapped.try(key)
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/phlexi/table.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zeitwerk"
4
- require "phlex"
5
4
  require "active_support/core_ext/object/blank"
6
5
 
7
6
  module Phlexi
@@ -9,16 +8,15 @@ module Phlexi
9
8
  Loader = Zeitwerk::Loader.new.tap do |loader|
10
9
  loader.tag = File.basename(__FILE__, ".rb")
11
10
  loader.inflector.inflect(
12
- "phlexi-display" => "Phlexi",
11
+ "phlexi-table" => "Phlexi",
13
12
  "phlexi" => "Phlexi",
14
- "dom" => "DOM"
13
+ "html" => "HTML"
15
14
  )
15
+ loader.ignore("#{__dir__}/table/field_options copy")
16
16
  loader.push_dir(File.expand_path("..", __dir__))
17
17
  loader.setup
18
18
  end
19
19
 
20
- COMPONENT_BASE = (defined?(::ApplicationComponent) ? ::ApplicationComponent : Phlex::HTML)
21
-
22
20
  NIL_VALUE = :__i_phlexi_display_nil_value_i__
23
21
 
24
22
  class Error < StandardError; end