phlexi-table 0.0.1 → 0.0.2

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