brick 1.0.75 → 1.0.76

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acd5fdf68935981a46106c879f5ac1fb44ac790c269c3eb2c5429cd67880f916
4
- data.tar.gz: 260ce60eeaf2e4b17ca5b5e76e54960c4d2685c9cb9d8549a35a16ce49b5a36c
3
+ metadata.gz: 615640b22db113a3959644ee9c2d08ff3a2b37aa10f12dc92d68effef091c228
4
+ data.tar.gz: b0d3e616b0f44584cf6960f8b8b191fc449b9afc426a6486da11c2deadece7a6
5
5
  SHA512:
6
- metadata.gz: 00f45ff020fcd729c2115f6d9f6ab0ab588247b2d727de7e1bd6acf53fa685ef76eeecce181a7a0c4659d8156971f88466915749cfad44e993993179435acdbd
7
- data.tar.gz: 74f0b40c44bae3729624ca0ba6a378f9067e4b0aa98460a03aa193e292432576327991d8c2a107317d8ce51ce223eee780a0d632d5805ca14b91a1b55388259f
6
+ metadata.gz: 02a9f1c74af24e1e23df8b64972d7af8d74bbb870f1510b69d0814f362959e2527f2a7ee9ff7ca65187eb97be4fad1136d64bea7e01a8327a57608924a8f12d8
7
+ data.tar.gz: '0265977b746d0955c76bef269067d14363cda4373fc5f726f19fb284baf31d0df3bdaae6ffad7301b71cd630a2745db4d30f1da79ec43ef4ae1a92bc986058e9'
data/lib/brick/config.rb CHANGED
@@ -90,6 +90,15 @@ module Brick
90
90
  @mutex.synchronize { @additional_references = references }
91
91
  end
92
92
 
93
+ # Custom columns to add to a table, minimally defined with a name and DSL string
94
+ def custom_columns
95
+ @mutex.synchronize { @custom_columns }
96
+ end
97
+
98
+ def custom_columns=(cust_cols)
99
+ @mutex.synchronize { @custom_columns = cust_cols }
100
+ end
101
+
93
102
  # Skip creating a has_many association for these
94
103
  def exclude_hms
95
104
  @mutex.synchronize { @exclude_hms }
@@ -100,73 +100,86 @@ module ActiveRecord
100
100
  dsl
101
101
  end
102
102
 
103
- def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, emit_dsl = false, is_polymorphic = false)
104
- build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
105
- build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
103
+ def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false, dsl = nil, emit_dsl = false)
104
+ unless build_array.is_a?(::Brick::JoinArray)
105
+ build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
106
+ build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
107
+ end
108
+ prefix = [prefix] unless prefix.is_a?(Array)
106
109
  members = []
110
+ unless dsl || (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
111
+ # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
112
+ x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
113
+ x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
114
+ return members
115
+ end
116
+
117
+ # Do the actual dirty work of recursing through nested DSL
107
118
  bracket_name = nil
108
- prefix = [prefix] unless prefix.is_a?(Array)
109
- if (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
110
- dsl2 = +'' # To replace our own DSL definition in case it needs to be expanded
111
- dsl3 = +'' # To return expanded DSL that is nested from another model
112
- klass = nil
113
- dsl.each_char do |ch|
114
- if bracket_name
115
- if ch == ']' # Time to process a bracketed thing?
116
- parts = bracket_name.split('.')
117
- first_parts = parts[0..-2].map do |part|
118
- klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
119
- puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
120
- part_sym
121
- end
122
- parts = prefix + first_parts + [parts[-1]]
123
- if parts.length > 1
124
- unless is_polymorphic
125
- s = build_array
126
- parts[0..-3].each { |v| s = s[v.to_sym] }
127
- s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
128
- end
129
- translations[parts[0..-2].join('.')] = klass
119
+ dsl2 = +'' # To replace our own DSL definition in case it needs to be expanded
120
+ dsl3 = +'' # To return expanded DSL that is nested from another model
121
+ klass = nil
122
+ dsl.each_char do |ch|
123
+ if bracket_name
124
+ if ch == ']' # Time to process a bracketed thing?
125
+ parts = bracket_name.split('.')
126
+ first_parts = parts[0..-2].map do |part|
127
+ klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
128
+ puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
129
+ part_sym
130
+ end
131
+ parts = prefix + first_parts + [parts[-1]]
132
+ if parts.length > 1
133
+ unless is_polymorphic
134
+ s = build_array
135
+ parts[0..-3].each { |v| s = s[v.to_sym] }
136
+ s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
130
137
  end
131
- if klass.column_names.exclude?(parts.last) &&
132
- (klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
133
- # Expand this entry which refers to an association name
134
- members2, dsl2a = klass.brick_parse_dsl(build_array, prefix + [possible_dsl], translations, true)
135
- members += members2
136
- dsl2 << dsl2a
137
- dsl3 << dsl2a
138
- else
139
- dsl2 << "[#{bracket_name}]"
140
- if emit_dsl
141
- dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
142
- end
143
- members << parts
138
+ translations[parts[0..-2].join('.')] = klass
139
+ end
140
+ if klass.column_names.exclude?(parts.last) &&
141
+ (klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
142
+ if prefix.empty? # Custom columns start with an empty prefix
143
+ prefix << parts.shift until parts.empty?
144
144
  end
145
- bracket_name = nil
145
+ # Expand this entry which refers to an association name
146
+ members2, dsl2a = klass.brick_parse_dsl(build_array, prefix + [possible_dsl], translations, is_polymorphic, nil, true)
147
+ members += members2
148
+ dsl2 << dsl2a
149
+ dsl3 << dsl2a
146
150
  else
147
- bracket_name << ch
151
+ dsl2 << "[#{bracket_name}]"
152
+ if emit_dsl
153
+ dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
154
+ end
155
+ members << parts
148
156
  end
149
- elsif ch == '['
150
- bracket_name = +''
151
- klass = self
157
+ bracket_name = nil
152
158
  else
153
- dsl2 << ch
154
- dsl3 << ch
159
+ bracket_name << ch
155
160
  end
161
+ elsif ch == '['
162
+ bracket_name = +''
163
+ klass = self
164
+ else
165
+ dsl2 << ch
166
+ dsl3 << ch
156
167
  end
157
- # Rewrite the DSL in case it's now different from having to expand it
158
- # if ::Brick.config.model_descrips[name] != dsl2
159
- # puts ::Brick.config.model_descrips[name]
160
- # puts dsl2.inspect
161
- # puts dsl3.inspect
162
- # binding.pry
163
- # end
164
- ::Brick.config.model_descrips[name] = dsl2 unless emit_dsl
165
- else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
166
- x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
167
- x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
168
168
  end
169
- emit_dsl ? [members, dsl3] : members
169
+ # Rewrite the DSL in case it's now different from having to expand it
170
+ # if ::Brick.config.model_descrips[name] != dsl2
171
+ # puts ::Brick.config.model_descrips[name]
172
+ # puts dsl2.inspect
173
+ # puts dsl3.inspect
174
+ # binding.pry
175
+ # end
176
+ if emit_dsl
177
+ # Had been: [members, dsl2, dsl3]
178
+ [members, dsl3]
179
+ else
180
+ ::Brick.config.model_descrips[name] = dsl2
181
+ members
182
+ end
170
183
  end
171
184
 
172
185
  # If available, parse simple DSL attached to a model in order to provide a friendlier name.
@@ -177,7 +190,8 @@ module ActiveRecord
177
190
  end
178
191
 
179
192
  def self.brick_descrip(obj, data = nil, pk_alias = nil)
180
- if (dsl = ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
193
+ dsl = obj if obj.is_a?(String)
194
+ if (dsl ||= ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
181
195
  idx = -1
182
196
  caches = {}
183
197
  output = +''
@@ -272,18 +286,28 @@ module ActiveRecord
272
286
  def _br_associatives
273
287
  @_br_associatives ||= {}
274
288
  end
289
+ # Custom columns
290
+ def _br_cust_cols
291
+ @_br_cust_cols ||= {}
292
+ end
275
293
  end
276
294
 
277
- # Search for BT, HM, and HMT DSL stuff
295
+ # Search for custom column, BT, HM, and HMT DSL stuff
278
296
  def self._brick_calculate_bts_hms(translations, join_array)
297
+ # Add any custom columns
298
+ ::Brick.config.custom_columns&.fetch(table_name, nil)&.each do |k, cc|
299
+ # false = not polymorphic, and true = yes -- please emit_dsl
300
+ pieces, my_dsl = brick_parse_dsl(join_array, [], translations, false, cc, true)
301
+ _br_cust_cols[k] = [pieces, my_dsl]
302
+ end
279
303
  bts, hms, associatives = ::Brick.get_bts_and_hms(self)
280
304
  bts.each do |_k, bt|
281
305
  next if bt[2] # Polymorphic?
282
306
 
283
307
  # join_array will receive this relation name when calling #brick_parse_dsl
284
308
  _br_bt_descrip[bt.first] = if bt[1].is_a?(Array)
285
- # Last two params here: "false" is for don't emit DSL, and "true" is for yes, we are polymorphic
286
- bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, false, true) }
309
+ # Last params here: "true" is for yes, we are polymorphic
310
+ bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
287
311
  else
288
312
  { bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
289
313
  end
@@ -314,7 +338,7 @@ module ActiveRecord
314
338
  else # Expecting only Symbol
315
339
  if _br_hm_counts.key?(ord_part)
316
340
  ord_part = "\"b_r_#{ord_part}_ct\""
317
- elsif !_br_bt_descrip.key?(ord_part) && !column_names.include?(ord_part.to_s)
341
+ elsif !_br_bt_descrip.key?(ord_part) && !_br_cust_cols.key?(ord_part) && !column_names.include?(ord_part.to_s)
318
342
  # Disallow ordering by a bogus column
319
343
  # %%% Note this bogus entry so that Javascript can remove any bogus _brick_order
320
344
  # parameter from the querystring, pushing it into the browser history.
@@ -331,6 +355,11 @@ module ActiveRecord
331
355
  [order_by, order_by_txt]
332
356
  end
333
357
 
358
+ def self.brick_select(params = {}, selects = [], *args)
359
+ (relation = all).brick_select(params, selects, *args)
360
+ relation.select(selects)
361
+ end
362
+
334
363
  private
335
364
 
336
365
  def self._brick_get_fks
@@ -421,7 +450,13 @@ module ActiveRecord
421
450
  end
422
451
  end
423
452
 
424
- def brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
453
+ def brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
454
+ is_add_bts = is_add_hms = true
455
+
456
+ # Build out cust_cols, bt_descrip and hm_counts now so that they are available on the
457
+ # model early in case the user wants to do an ORDER BY based on any of that.
458
+ model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
459
+
425
460
  is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
426
461
  is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
427
462
  is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
@@ -446,7 +481,7 @@ module ActiveRecord
446
481
  end
447
482
 
448
483
  # %%% Skip the metadata columns
449
- if selects&.empty? # Default to all columns
484
+ if selects.empty? # Default to all columns
450
485
  tbl_no_schema = table.name.split('.').last
451
486
  # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
452
487
  # ActiveRecord::StatementInvalid (TinyTds::Error: DBPROCESS is dead or not enabled)
@@ -481,7 +516,36 @@ module ActiveRecord
481
516
  id_for_tables = Hash.new { |h, k| h[k] = [] }
482
517
  field_tbl_names = Hash.new { |h, k| h[k] = {} }
483
518
  used_col_aliases = {} # Used to make sure there is not a name clash
484
- bt_columns = klass._br_bt_descrip.each_with_object([]) do |v, s|
519
+
520
+ # CUSTOM COLUMNS
521
+ # ==============
522
+ klass._br_cust_cols.each do |k, cc|
523
+ if respond_to?(k) # Name already taken?
524
+ # %%% Use ensure_unique here in this kind of fashion:
525
+ # cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
526
+ # binding.pry
527
+ next
528
+ end
529
+
530
+ cc.first.each do |cc_part|
531
+ dest_klass = cc_part[0..-2].inject(klass) { |kl, cc_part_term| kl.reflect_on_association(cc_part_term).klass }
532
+ tbl_name = (field_tbl_names[k][cc_part.last] ||= shift_or_first(chains[dest_klass])).split('.').last
533
+ # Deal with the conflict if there are two parts in the custom column named the same,
534
+ # "category.name" and "product.name" for instance will end up with aliases of "name"
535
+ # and "product__name".
536
+ cc_part_idx = cc_part.length - 1
537
+ while cc_part_idx > 0 &&
538
+ (col_alias = "br_cc_#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__')}") &&
539
+ used_col_aliases.key?(col_alias)
540
+ cc_part_idx -= 1
541
+ end
542
+ selects << "#{tbl_name}.#{cc_part.last} AS #{col_alias}"
543
+ cc_part << col_alias
544
+ used_col_aliases[col_alias] = nil
545
+ end
546
+ end
547
+
548
+ klass._br_bt_descrip.each do |v|
485
549
  v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
486
550
  next if chains[k1].nil?
487
551
 
@@ -544,7 +608,13 @@ module ActiveRecord
544
608
  klass._br_hm_counts.each do |k, hm|
545
609
  associative = nil
546
610
  count_column = if hm.options[:through]
547
- hm.foreign_key if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
611
+ if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
612
+ if hm.source_reflection.macro == :belongs_to # Traditional HMT using an associative table
613
+ hm.foreign_key
614
+ else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems
615
+ hm.source_reflection.active_record.primary_key
616
+ end
617
+ end
548
618
  else
549
619
  fk_col = hm.foreign_key
550
620
  poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
@@ -596,7 +666,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
596
666
  end
597
667
  where!(wheres) unless wheres.empty?
598
668
  # Must parse the order_by and see if there are any symbols which refer to BT associations
599
- # as they must be expanded to find the corresponding b_r_model__column naming for each.
669
+ # or custom columns as they must be expanded to find the corresponding b_r_model__column
670
+ # or br_cc_column naming for each.
600
671
  if order_by.present?
601
672
  final_order_by = *order_by.each_with_object([]) do |v, s|
602
673
  if v.is_a?(Symbol)
@@ -605,6 +676,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
605
676
  bt_cols.values.each do |v1|
606
677
  v1.each { |v2| s << "\"#{v2.last}\"" if v2.length > 1 }
607
678
  end
679
+ elsif (cc_cols = klass._br_cust_cols[v])
680
+ cc_cols.first.each { |v1| s << "\"#{v1.last}\"" if v1.length > 1 }
608
681
  else
609
682
  s << v
610
683
  end
@@ -1204,22 +1277,6 @@ class Object
1204
1277
  return
1205
1278
  end
1206
1279
 
1207
- # Normal (non-swagger) request
1208
-
1209
- # We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
1210
- # wants to do an ORDER BY based on any of that
1211
- translations = {}
1212
- join_array = ::Brick::JoinArray.new
1213
- is_add_bts = is_add_hms = true
1214
- # This builds out bt_descrip and hm_counts on the model
1215
- model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
1216
-
1217
- # %%% Allow params to define which columns to use for order_by
1218
- # Overriding the default by providing a querystring param?
1219
- ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
1220
- order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
1221
-
1222
- ::Brick.set_db_schema(params)
1223
1280
  if request.format == :csv # Asking for a template?
1224
1281
  require 'csv'
1225
1282
  exported_csv = CSV.generate(force_quotes: false) do |csv_out|
@@ -1233,7 +1290,16 @@ class Object
1233
1290
  return
1234
1291
  end
1235
1292
 
1236
- @_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), order_by, translations, join_array)
1293
+ # Normal (not swagger or CSV) request
1294
+
1295
+ # %%% Allow params to define which columns to use for order_by
1296
+ # Overriding the default by providing a querystring param?
1297
+ ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
1298
+ order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
1299
+
1300
+ @_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), nil,
1301
+ translations = {},
1302
+ join_array = ::Brick::JoinArray.new)
1237
1303
  # %%% Add custom HM count columns
1238
1304
  # %%% What happens when the PK is composite?
1239
1305
  counts = model._br_hm_counts.each_with_object([]) do |v, s|
@@ -1902,9 +1968,7 @@ module Brick
1902
1968
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
1903
1969
  pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
1904
1970
  pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl&.singularize != bt_assoc_name
1905
- cnstr_base = cnstr_name = "(brick) #{for_tbl}_#{pri_tbl}"
1906
- cnstr_added_num = 1
1907
- cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
1971
+ cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
1908
1972
  missing = []
1909
1973
  missing << fk[1] unless relations.key?(fk[1])
1910
1974
  missing << primary_table unless is_class || relations.key?(primary_table)
@@ -2029,6 +2093,28 @@ module Brick
2029
2093
  ::Brick.relations.keys.map { |v| [(r = v.pluralize), (model = models[r])&.last&.table_name || v, migrations&.fetch(r, nil), model&.first] }
2030
2094
  end
2031
2095
 
2096
+ def ensure_unique(name, *sources)
2097
+ base = name
2098
+ if (added_num = name.slice!(/_(\d+)$/))
2099
+ added_num = added_num[1..-1].to_i
2100
+ else
2101
+ added_num = 1
2102
+ end
2103
+ while (
2104
+ name = "#{base}_#{added_num += 1}"
2105
+ sources.each_with_object(nil) do |v, s|
2106
+ s || case v
2107
+ when Hash
2108
+ v.key?(name)
2109
+ when Array
2110
+ v.include?(name)
2111
+ end
2112
+ end
2113
+ )
2114
+ end
2115
+ name
2116
+ end
2117
+
2032
2118
  # Locate orphaned records
2033
2119
  def find_orphans(multi_schema)
2034
2120
  is_default_schema = multi_schema&.==(Apartment.default_schema)
@@ -33,6 +33,9 @@ module Brick
33
33
  # Additional references (virtual foreign keys)
34
34
  ::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)
35
35
 
36
+ # Custom columns to add to a table, minimally defined with a name and DSL string
37
+ ::Brick.custom_columns = app.config.brick.fetch(:custom_columns, nil)
38
+
36
39
  # When table names have specific prefixes, automatically place them in their own module with a table_name_prefix.
37
40
  ::Brick.order = app.config.brick.fetch(:order, {})
38
41
 
@@ -249,8 +252,8 @@ tr th {
249
252
  right: 0;
250
253
  cursor: pointer;
251
254
  }
252
- #headerTop tr th:hover {
253
- background-color: #18B090;
255
+ #headerTop tr th:hover, #headerTop tr th.highlight {
256
+ background-color: #28B898;
254
257
  }
255
258
  #exclusions {
256
259
  font-size: 0.7em;
@@ -273,6 +276,10 @@ tr th, tr td {
273
276
  padding: 0.2em 0.5em;
274
277
  }
275
278
 
279
+ tr td.highlight {
280
+ background-color: #B0B0FF;
281
+ }
282
+
276
283
  .show-field {
277
284
  background-color: #004998;
278
285
  }
@@ -500,6 +507,34 @@ function changeout(href, param, value, trimAfter) {
500
507
  var grid = document.getElementById(\"#{table_name}\");
501
508
  #{table_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
502
509
  var headerTop = document.getElementById(\"headerTop\");
510
+ var headerCols;
511
+ if (grid) {
512
+ // COLUMN HEADER AND TABLE CELL HIGHLIGHTING
513
+ var gridHighHeader = null,
514
+ gridHighCell = null;
515
+ grid.addEventListener(\"mouseenter\", gridMove);
516
+ grid.addEventListener(\"mousemove\", gridMove);
517
+ grid.addEventListener(\"mouseleave\", function (evt) {
518
+ if (gridHighCell) gridHighCell.classList.remove(\"highlight\");
519
+ gridHighCell = null;
520
+ if (gridHighHeader) gridHighHeader.classList.remove(\"highlight\");
521
+ gridHighHeader = null;
522
+ });
523
+ function gridMove(evt) {
524
+ var lastHighCell = gridHighCell;
525
+ gridHighCell = document.elementFromPoint(evt.x, evt.y);
526
+ if (lastHighCell !== gridHighCell) {
527
+ gridHighCell.classList.add(\"highlight\");
528
+ if (lastHighCell) lastHighCell.classList.remove(\"highlight\");
529
+ }
530
+ var lastHighHeader = gridHighHeader;
531
+ gridHighHeader = headerCols[gridHighCell.cellIndex];
532
+ if (lastHighHeader !== gridHighHeader) {
533
+ if (gridHighHeader) gridHighHeader.classList.add(\"highlight\");
534
+ if (lastHighHeader) lastHighHeader.classList.remove(\"highlight\");
535
+ }
536
+ }
537
+ }
503
538
  function setHeaderSizes() {
504
539
  // console.log(\"start\");
505
540
  // See if the headerTop is already populated
@@ -531,6 +566,7 @@ function setHeaderSizes() {
531
566
  }
532
567
  }
533
568
  }
569
+ headerCols = tr.childNodes;
534
570
  if (isEmpty) headerTop.appendChild(tr);
535
571
  }
536
572
  grid.style.marginTop = \"-\" + getComputedStyle(headerTop).height;
@@ -779,7 +815,8 @@ erDiagram
779
815
  cols[col_name] = col
780
816
  end
781
817
  unless @_brick_sequence # If no sequence is defined, start with all inclusions
782
- @_brick_sequence = col_keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
818
+ cust_cols = #{model_name}._br_cust_cols
819
+ @_brick_sequence = col_keys + cust_cols.keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
783
820
  end
784
821
  @_brick_sequence.reject! { |nm| @_brick_excl.include?(nm) } if @_brick_excl # Reject exclusions
785
822
  @_brick_sequence.each_with_object(+'') do |col_name, s|
@@ -796,6 +833,8 @@ erDiagram
796
833
  elsif col # HM column
797
834
  s << \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col[2]} \"
798
835
  s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1]._brick_index}_path\"))}\")
836
+ elsif (cc = cust_cols.key?(col_name)) # Custom column
837
+ s << \"<th x-order=\\\"#\{col_name}\\\">#\{col_name}\"
799
838
  else # Bad column name!
800
839
  s << \"<th title=\\\"<< Unknown column >>\\\">#\{col_name}\"
801
840
  end
@@ -813,7 +852,7 @@ erDiagram
813
852
  <td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
814
853
  <% @_brick_sequence.each do |col_name|
815
854
  val = #{obj_name}.attributes[col_name] %>
816
- <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name)%>><%
855
+ <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name) || (cust_col = cust_cols[col_name])%>><%
817
856
  if (bt = bts[col_name])
818
857
  if bt[2] # Polymorphic?
819
858
  bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
@@ -847,6 +886,9 @@ erDiagram
847
886
  elsif (col = cols[col_name])
848
887
  col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
849
888
  %><%= display_value(col_type || col&.sql_type, val) %><%
889
+ elsif cust_col
890
+ data = cust_col.first.map { |cc_part| #{obj_name}.send(cc_part.last) }
891
+ %><%= #{model_name}.brick_descrip(cust_col.last, data) %><%
850
892
  else # Bad column name!
851
893
  %>?<%
852
894
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 75
8
+ TINY = 76
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
data/lib/brick.rb CHANGED
@@ -329,6 +329,15 @@ module Brick
329
329
  end
330
330
  end
331
331
 
332
+ # Custom columns to add to a table, minimally defined with a name and DSL string.
333
+ # @api public
334
+ def custom_columns=(cust_cols)
335
+ if cust_cols
336
+ cust_cols = cust_cols.call if cust_cols.is_a?(Proc)
337
+ Brick.config.custom_columns = cust_cols
338
+ end
339
+ end
340
+
332
341
  # @api public
333
342
  def order=(value)
334
343
  Brick.config.order = value
@@ -207,6 +207,12 @@ module Brick
207
207
  # # to be the primary key.)
208
208
  #{bar}
209
209
 
210
+ # # Custom columns to add to a table, minimally defined with a name and DSL string.
211
+ # Brick.custom_columns = { 'users' => { messages: ['[COUNT(messages)] messages', 'messages'] },
212
+ # 'orders' => { salesperson: '[salesperson.first] [salesperson.last]',
213
+ # products: ['[COUNT(order_items.product)] products', 'order_items.product' ] }
214
+ # }
215
+
210
216
  # # Skip creating a has_many association for these (only retain the belongs_to built from this additional_reference).
211
217
  # # (Uses the same exact three-part format as would define an additional_reference)
212
218
  # # Say for instance that we didn't care to display the favourite colours that users have:
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.75
4
+ version: 1.0.76
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-30 00:00:00.000000000 Z
11
+ date: 2022-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord