hot-glue 0.5.26 → 0.6.0

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: 226e0b7e11441c4332105233fc63eb44f879d582c4ce791e18e56bd54de56627
4
- data.tar.gz: a1befc9f7e6d059b502730f4d7411d7aab538b8a239572163e112d85a364d39a
3
+ metadata.gz: 822437dfda890941c29f2ff18b927c60a84279306c0937d354f69e18c67f51e5
4
+ data.tar.gz: 193b9bb92313ae7440c72d8d7598f918400136f7edabc70bf7ec7def1b4f96ed
5
5
  SHA512:
6
- metadata.gz: fb0ce5e6cf19d8528f3a7cdf421f4cdaf81a10c8670b132a6994ee178361e353b9ffba58de34bf14c8f422bae485bd4994c5c55a06240b2f0ab358419fd2660f
7
- data.tar.gz: b9bc2326b8d1b8430e278a3fc9b74a6a198238fdfc015e21b37024761283f1dbb770b15e4a90bbf8bfebcb6acf5e110a704409e3a22093067fdd0d6ddd4ad4ff
6
+ metadata.gz: '0210383122eded31e46b9cc02bc6a7c230d95936f13635149c884774e762b178eb326e3722466a6ff8e365fd3d45782a05c0a60733d678499762ff49d6f7b58d'
7
+ data.tar.gz: 8cc9ed4dc85875d66dbd66ecac22908bfd6a8759588e37a2b26bb03522a16b8559ca0bc0ee0311a28a627f2f25af703ed744ea1aba6f8f1d48c5fbc35d28748a
data/README.md CHANGED
@@ -772,7 +772,7 @@ Note that Hot Glue still generates a singular partial (`_form`) for both actions
772
772
  <% end %>
773
773
  ```
774
774
 
775
- This works for both regular fields, association fields, and alt lookup fields.
775
+ This works for both regular fields, association fields.
776
776
 
777
777
 
778
778
  When mixing the show only, update show only, and Pundit features, notice that the show only + update show only will act to override whatever the policy might say.
@@ -954,38 +954,6 @@ This happens using two interconnected mechanisms:
954
954
  please note that *creating* and *deleting* do not yet have a full & complete implementation: Your pages won't re-render the pages being viewed cross-peer (that is, between two users using the app at the same time) if the insertion or deletion causes the pagination to be off for another user.
955
955
 
956
956
 
957
- ### `--alt-foreign-key-lookup` (Foreign Key Lookups)
958
-
959
- Example #1
960
- `--alt-foreign-key-lookup=user_id{email}`
961
-
962
- Let's assume a `Company` `has_many :company_users` and also a `Company` `has_many :users, through: :company_users`
963
-
964
- Normally, you would be constructing a CompanyUsers downnest portal on the Company page. (Showing you only CompanyUsers associated with that company.)
965
-
966
- A drop down of _all users in the_ database will be display on the screen where you create a new CompanyUser (join) record.
967
-
968
- Let's say instead you don't want to expose the full list of all users to this controller, but instead make your user enter the full email address of the user to identify them.
969
-
970
- Instead of a drop-down, the interface will present an input box for the user to supply an *search by* email.
971
-
972
- Example #2
973
- ```
974
- --alt-foreign-key-lookup='agent_id{email+}'
975
- ```
976
-
977
- First, assume the current able has a `belongs_to` association for `agent_id` to a foreign model, `Agent`
978
-
979
- Here, we would build a scaffold that would treat the foreign key `agent_id` as an alt lookup, allowing the user to search for foreign records by email (the agent's email).
980
-
981
- The `+` symbol indicates to automatically make a new `agent` record (without it, the agent_id will be set to nil if there is no associated agent record found -- this may cause the update to fail, unless `optional: true` is on the belongs_to association.)
982
-
983
-
984
-
985
- ** Note: Current implementation does not work in conjunction with hawked associations to protect against users from accessing associated records not within scope.**
986
- TODO: make it work with hawked associations to protect against users from accessing associated records not within scope
987
-
988
-
989
957
  ## "Thing" Label
990
958
 
991
959
  Note that on a per model basis, you can also globally omit the label or set a unique label value using
@@ -1256,7 +1224,7 @@ end
1256
1224
  ```
1257
1225
 
1258
1226
 
1259
- If you are using factory creation along with with alt lookups, be sure your factory code creates a local variable that follows this name
1227
+ be sure your factory code creates a local variable that follows this name
1260
1228
 
1261
1229
  **<downcase association name>**_factory.<downcase association name>
1262
1230
 
@@ -1410,6 +1378,33 @@ end
1410
1378
 
1411
1379
  ```
1412
1380
 
1381
+ ### Typeahead Foreign Keys
1382
+
1383
+ Let's go back to the first Books & Authors example.
1384
+ assuming you have created
1385
+ `bin/rails generate model Book title:string author_id:integer`
1386
+ and
1387
+ `bin/rails generate model Author name:string`
1388
+ and also added `has_many :books` to Author and `belongs_to :author` to Book
1389
+
1390
+
1391
+ You can now use a typeahead when editing the book. Instead of displaying the authors in a drop-down list, the authors will appear in a searchable typehead.
1392
+
1393
+ You will do these three things:
1394
+
1395
+ 1. As a one-time setup step for your app, run
1396
+ `bin/rails generate hot_glue:install_typeahead`
1397
+ 2. When generating a scaffold you want to make a typeahead association, use `--modify='parent_id{typeahead}'` where `parent_id` is the foreign key
1398
+ `bin/rails generate hot_glue:scaffold Book --include=title,author_id --modify='author_id{typeahead}'`
1399
+ 3. Within each namespace, you will generate a special typeahead controller (it exists for the associated object to be searched on
1400
+ `bin/rails generate hot_glue:typehead Author`
1401
+ This will create a controller for `AuthorsTypeaheadController` that will allow text search against any *string* field on the `Author` model.
1402
+ This special generator takes flags `--namespace` as the normal generator and also `--search-by` to let you specify the list of fields you want to search by.
1403
+
1404
+ Your new and edit views that were built on books now give you a search box for the author spot. Notice that just making the selection
1405
+ puts the value into the search box and the id into a hidden field.
1406
+
1407
+ You need to making a selection *and* click "Save" to update the record.
1413
1408
 
1414
1409
  ### TinyMCE
1415
1410
  1. `bundle add tinymce-rails` to add it to your Gemfile
@@ -1469,6 +1464,30 @@ bin/rails generate Thing --include=my_story --modify='my_story{tinymce}'
1469
1464
 
1470
1465
  # VERSION HISTORY
1471
1466
 
1467
+ #### 2023-11-03 - v0.6.0
1468
+
1469
+ Typeahead Associations
1470
+
1471
+ You can now use a typeahead when editing any foreign key.
1472
+
1473
+ Instead of displaying the foreign key in a drop-down list, the associated record is now selectable
1474
+ from a searchable typehead input.
1475
+
1476
+ The typeahead is implemented with a native Stimulus JS pair of controllers and is a modern & clean replacement to the old typeahead options.
1477
+
1478
+ 1. As a one-time setup step for your app, run
1479
+ `bin/rails generate hot_glue:install_typeahead`
1480
+ 2. When generating a scaffold you want to make a typeahead association, use `--modify='parent_id{typeahead}'` where `parent_id` is the foreign key
1481
+ `bin/rails generate hot_glue:scaffold Book --include=title,author_id --modify='author_id{typeahead}'`
1482
+ 3. Within each namespace, you will generate a special typeahead controller (it exists for the associated object to be searched on
1483
+ `bin/rails generate hot_glue:typehead Author`
1484
+ This will create a controller for `AuthorsTypeaheadController` that will allow text search against any *string* field on the `Author` model.
1485
+ This special generator takes flags `--namespace` like the normal generator and also `--search-by` to let you specify the list of fields you want to search by.
1486
+
1487
+ The example Books & Authors app with typeahead is here:
1488
+
1489
+ https://github.com/hot-glue-for-rails/BooksAndAuthorsWithTypeahead2
1490
+
1472
1491
 
1473
1492
  #### 2023-10-23 - v0.5.26
1474
1493
 
@@ -53,7 +53,6 @@ class FieldFactory
53
53
  hawk_keys: generator.hawk_keys,
54
54
  auth: generator.auth,
55
55
  class_name: generator.singular_class,
56
- alt_lookups: generator.alt_lookups,
57
56
  singular: generator.singular,
58
57
  self_auth: generator.self_auth,
59
58
  update_show_only: generator.update_show_only,
@@ -61,6 +60,7 @@ class FieldFactory
61
60
  sample_file_path: generator.sample_file_path,
62
61
  modify_as: generator.modify_as[name.to_sym] || nil,
63
62
  display_as: generator.display_as[name.to_sym] || nil,
64
- default_boolean_display: generator.default_boolean_display)
63
+ default_boolean_display: generator.default_boolean_display,
64
+ namespace: generator.namespace_value)
65
65
  end
66
66
  end
@@ -5,12 +5,12 @@ class AssociationField < Field
5
5
 
6
6
  attr_accessor :assoc_name, :assoc_class, :assoc
7
7
 
8
- def initialize(alt_lookups: , class_name: , default_boolean_display:, display_as: ,
8
+ def initialize( class_name: , default_boolean_display:, display_as: ,
9
9
  name: , singular: ,
10
10
  update_show_only: ,
11
11
  hawk_keys: , auth: , sample_file_path:, ownership_field: ,
12
12
  attachment_data: nil , layout_strategy: , form_placeholder_labels: nil,
13
- form_labels_position:, modify_as: , self_auth: )
13
+ form_labels_position:, modify_as: , self_auth: , namespace: )
14
14
  super
15
15
 
16
16
  @assoc_model = eval("#{class_name}.reflect_on_association(:#{assoc})")
@@ -48,10 +48,7 @@ class AssociationField < Field
48
48
 
49
49
  def spec_setup_and_change_act(which_partial)
50
50
  if which_partial == :update && update_show_only.include?(name)
51
- # do not update tests
52
- elsif alt_lookups.keys.include?(name.to_s)
53
- lookup = alt_lookups[name.to_s][:lookup_as]
54
- " find(\"[name='#{singular}[__lookup_#{lookup}]']\").fill_in( with: #{assoc}1.#{lookup} )"
51
+
55
52
  else
56
53
  " #{name}_selector = find(\"[name='#{singular}[#{name}]']\").click \n" +
57
54
  " #{name}_selector.first('option', text: #{assoc}1.name).select_option"
@@ -82,10 +79,21 @@ class AssociationField < Field
82
79
  assoc_name = name.to_s.gsub("_id","")
83
80
  assoc = eval("#{class_name}.reflect_on_association(:#{assoc_name})")
84
81
 
85
-
86
- if @alt_lookups.keys.include?(name.to_s)
87
- alt = @alt_lookups[name.to_s][:lookup_as]
88
- "<%= f.text_field :__lookup_#{alt}, value: @#{singular}.#{assoc_name}.try(:#{alt}), placeholder: \"search by #{alt}\" %>"
82
+ if modify_as && modify_as[:typeahead]
83
+ search_url = "#{namespace ? namespace + "_" : ""}#{assoc.plural_name}_typeahead_index_url"
84
+
85
+ "<div class='typeahead typeahead--#{singular}--#{assoc.name}_id'
86
+ data-controller='typeahead'
87
+ data-typeahead-url-value='<%= #{search_url} %>'
88
+ data-typeahead-typeahead-results-outlet='#search-results'>
89
+ <%= text_field_tag :#{assoc.plural_name}_query, '', placeholder: 'Search #{assoc.plural_name}', class: 'search__input',
90
+ data: { action: 'keyup->typeahead#fetchResults keydown->typeahead#navigateResults', typeahead_target: 'query' },
91
+ autofocus: true,
92
+ autocomplete: 'off',
93
+ value: #{singular}.try(:#{assoc.name}).try(:name) %>
94
+ <%= f.hidden_field :#{assoc.name}_id, value: #{singular}.try(:#{assoc.name}).try(:id), 'data-typeahead-target': 'hiddenFormValue' %>
95
+ <div data-typeahead-target='results'></div>
96
+ </div>"
89
97
  else
90
98
  if assoc.nil?
91
99
  exit_message = "*** Oops. on the #{class_name} object, there doesn't seem to be an association called '#{assoc_name}'"
@@ -1,10 +1,10 @@
1
1
  class AttachmentField < Field
2
2
  attr_accessor :attachment_data
3
- def initialize(name:, class_name:, alt_lookups:, default_boolean_display: ,
3
+ def initialize(name:, class_name:, default_boolean_display: ,
4
4
  display_as:,
5
5
  singular:, update_show_only:, hawk_keys:, auth:,
6
6
  sample_file_path: nil, attachment_data:, ownership_field:, layout_strategy: ,
7
- form_placeholder_labels: , form_labels_position:, modify_as:, self_auth: )
7
+ form_placeholder_labels: , form_labels_position:, modify_as:, self_auth: , namespace: )
8
8
  super
9
9
 
10
10
  @attachment_data = attachment_data
@@ -5,11 +5,10 @@ class Field
5
5
  :hawk_keys, :layout_strategy, :limit, :modify_as, :name, :object, :sample_file_path,
6
6
  :self_auth,
7
7
  :singular_class, :singular, :sql_type, :ownership_field,
8
- :update_show_only
8
+ :update_show_only, :namespace
9
9
 
10
10
  def initialize(
11
11
  auth: ,
12
- alt_lookups: ,
13
12
  attachment_data: nil,
14
13
  class_name: ,
15
14
  default_boolean_display: ,
@@ -24,7 +23,8 @@ class Field
24
23
  sample_file_path: nil,
25
24
  singular: ,
26
25
  update_show_only:,
27
- self_auth:
26
+ self_auth:,
27
+ namespace:
28
28
  )
29
29
  @name = name
30
30
  @layout_strategy = layout_strategy
@@ -43,6 +43,7 @@ class Field
43
43
 
44
44
  @self_auth = self_auth
45
45
  @default_boolean_display = default_boolean_display
46
+ @namesapce = namespace
46
47
 
47
48
  # TODO: remove knowledge of subclasses from Field
48
49
  unless self.class == AttachmentField
@@ -8,7 +8,7 @@ module HotGlue
8
8
  :inline_list_labels, :layout_object,
9
9
  :columns, :col_identifier, :singular,
10
10
  :form_placeholder_labels, :hawk_keys, :update_show_only,
11
- :alt_lookups, :attachments, :show_only, :columns_map, :pundit
11
+ :attachments, :show_only, :columns_map, :pundit
12
12
 
13
13
 
14
14
  def initialize(singular:, singular_class: ,
@@ -17,7 +17,7 @@ module HotGlue
17
17
  ownership_field: , form_labels_position: ,
18
18
  inline_list_labels: ,
19
19
  form_placeholder_labels:, hawk_keys: ,
20
- update_show_only:, alt_lookups: , attachments: , columns_map:, pundit: )
20
+ update_show_only:, attachments: , columns_map:, pundit: )
21
21
 
22
22
  @singular = singular
23
23
  @singular_class = singular_class
@@ -38,7 +38,6 @@ module HotGlue
38
38
  @form_placeholder_labels = form_placeholder_labels
39
39
  @hawk_keys = hawk_keys
40
40
  @update_show_only = update_show_only
41
- @alt_lookups = alt_lookups
42
41
  @attachments = attachments
43
42
  end
44
43
 
@@ -16,14 +16,16 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
16
16
  hook_for :form_builder, :as => :scaffold
17
17
 
18
18
  source_root File.expand_path('templates', __dir__)
19
- attr_accessor :alt_lookups, :attachments, :auth, :big_edit, :button_icons, :bootstrap_column_width, :columns,
19
+ attr_accessor :attachments, :auth, :big_edit, :button_icons, :bootstrap_column_width, :columns,
20
20
  :default_boolean_display,
21
21
  :display_as, :downnest_children, :downnest_object, :hawk_keys, :layout_object,
22
22
  :modify_as,
23
23
  :nest_with, :path, :plural, :sample_file_path, :show_only_data, :singular,
24
24
  :singular_class, :smart_layout, :stacked_downnesting, :update_show_only, :ownership_field,
25
25
  :layout_strategy, :form_placeholder_labels, :form_labels_position, :pundit,
26
- :self_auth
26
+ :self_auth, :namespace_value
27
+ # important: using an attr_accessor called :namespace indirectly causes a conflict with Rails class_name method
28
+ # so we use namespace_value instead
27
29
 
28
30
  class_option :singular, type: :string, default: nil
29
31
  class_option :plural, type: :string, default: nil
@@ -160,6 +162,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
160
162
 
161
163
  @plural = options['plural'] || @singular.pluralize # respects what you set in inflections.rb, to override, use plural option
162
164
  @namespace = options['namespace'] || nil
165
+ @namespace_value = @namespace
163
166
  use_controller_name = plural.titleize.gsub(" ", "")
164
167
  @controller_build_name = ((@namespace.titleize.gsub(" ", "") + "::" if @namespace) || "") + use_controller_name + "Controller"
165
168
  @controller_build_folder = use_controller_name.underscore
@@ -224,6 +227,10 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
224
227
  @modify_as[key.to_sym] = {enum: :partials}
225
228
  elsif $2 == "tinymce"
226
229
  @modify_as[key.to_sym] = {tinymce: 1}
230
+ elsif $2 == "typeahead"
231
+ @modify_as[key.to_sym] = {typeahead: 1}
232
+
233
+
227
234
  else
228
235
  raise "unknown modification direction #{$2}"
229
236
  end
@@ -251,26 +258,6 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
251
258
  # instead of a drop-down for the foreign entity, a text field will be presented
252
259
  # You must ALSO use a factory that contains a parameter of the same name as the 'value' (for example, `xyz_email`)
253
260
 
254
- alt_lookups_entry = options['alt_foreign_key_lookup'].split(",")
255
- @alt_lookups = {}
256
- @alt_foreign_key_lookup = alt_lookups_entry.each do |setting|
257
- setting =~ /(.*){(.*)}/
258
- key, lookup_as = $1, $2
259
- assoc = eval("#{class_name}.reflect_on_association(:#{key.to_s.gsub("_id", "")}).class_name")
260
-
261
- data = { lookup_as: lookup_as.gsub("+", ""),
262
- assoc: assoc,
263
- with_create: lookup_as.include?("+") }
264
- @alt_lookups[key] = data
265
- end
266
-
267
- puts "------ ALT LOOKUPS for #{@alt_lookups}"
268
-
269
- @update_alt_lookups = @alt_lookups.collect { |key, value|
270
- @update_show_only.include?(key) ?
271
- { key: value }
272
- : nil }.compact
273
-
274
261
  @label = options['label'] || (eval("#{class_name}.class_variable_defined?(:@@table_label_singular)") ? eval("#{class_name}.class_variable_get(:@@table_label_singular)") : singular.gsub("_", " ").titleize)
275
262
  @list_label_heading = options['list_label_heading'] || (eval("#{class_name}.class_variable_defined?(:@@table_label_plural)") ? eval("#{class_name}.class_variable_get(:@@table_label_plural)") : plural.gsub("_", " ").upcase)
276
263
 
@@ -449,6 +436,25 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
449
436
  @columns_map[col] = this_column_object.field
450
437
  end
451
438
 
439
+ @columns_map.each do |key, field|
440
+ if field.is_a?(AssociationField)
441
+ if @modify_as && @modify_as[key] && @modify_as[key][:typeahead]
442
+ assoc_name = field.assoc_name
443
+ file_path = "app/controllers/#{namespace ? namspace + "/" : ""}#{assoc_name.pluralize}_typeahead_controller.rb"
444
+
445
+ if ! File.exist?(file_path)
446
+
447
+ assoc_model = eval("#{class_name}.reflect_on_association(:#{field.assoc_name})")
448
+ assoc_class = assoc_model.class_name
449
+ puts "##############################################"
450
+ puts "WARNING: you specified --modify=#{key}{typeahead} but there is no file at `#{file_path}`; please create one with:"
451
+ puts "bin/rails generate hot_glue:typeahead #{assoc_class} #{namespace ? " --namespace=\#{namespace}" : ""}"
452
+ puts "##############################################"
453
+ end
454
+ end
455
+ end
456
+ end
457
+
452
458
  # create the template object
453
459
  if @markup == "erb"
454
460
  @template_builder = HotGlue::ErbTemplate.new(
@@ -464,7 +470,6 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
464
470
  ownership_field: @ownership_field,
465
471
  form_labels_position: @form_labels_position,
466
472
  form_placeholder_labels: @form_placeholder_labels,
467
- alt_lookups: @alt_lookups,
468
473
  attachments: @attachments,
469
474
  columns_map: @columns_map,
470
475
  pundit: @pundit,
@@ -672,24 +677,18 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
672
677
  end
673
678
 
674
679
  def fields_filtered_for_email_lookups
675
- @columns.reject { |c| @alt_lookups.keys.include?(c) } + @alt_lookups.values.map { |v| ("__lookup_#{v[:lookup_as]}").to_sym }
680
+ @columns
676
681
  end
677
682
 
678
683
  def creation_syntax
679
- merge_with = @alt_lookups.collect { |key, data|
680
- "#{data[:assoc].downcase}: #{data[:assoc].downcase}_factory.#{data[:assoc].downcase}"
681
- }.join(", ")
682
-
683
684
  if @factory_creation == ''
684
- "@#{singular } = #{ class_name }.create(modified_params#{'.merge(' + merge_with + ')' if !merge_with.empty?})"
685
+ "@#{singular } = #{ class_name }.create(modified_params)"
685
686
  else
686
687
  "#{@factory_creation}\n" +
687
688
  " @#{singular } = factory.#{singular}"
688
689
  end
689
690
  end
690
691
 
691
-
692
-
693
692
  def formats
694
693
  [format]
695
694
  end
@@ -1285,11 +1284,7 @@ class HotGlue::ScaffoldGenerator < Erb::Generators::ScaffoldGenerator
1285
1284
  }.join("")
1286
1285
  end
1287
1286
 
1288
- def controller_update_params_tap_away_alt_lookups
1289
- @alt_lookups.collect { |key, data|
1290
- ".tap{ |ary| ary.delete('__lookup_#{data[:lookup_as]}') }"
1291
- }.join("")
1292
- end
1287
+
1293
1288
 
1294
1289
  def nested_for_turbo_id_list_constructor
1295
1290
  if @nested_set.any?
@@ -98,15 +98,10 @@ class <%= controller_class_name %> < <%= controller_descends_from %>
98
98
  end
99
99
 
100
100
  def create
101
- <% if @alt_lookups.any? %><%= @alt_lookups.collect{|key, data|
102
- " #{data[:assoc].downcase} = #{data[:assoc]}.#{data[:with_create] ? "find_or_create_by" : "find_by"}(#{data[:lookup_as]}: #{ singular_name }_params[:__lookup_#{data[:lookup_as]}])\n"
103
- }.join("/n") %><% end %> <% merge_lookups = @alt_lookups.collect{|key, data| "#{key.gsub("_id", "")}: #{key.gsub("_id", "")}" }.join(",") %>
104
- modified_params = modify_date_inputs_on_params(<%= singular_name %>_params.dup<%= controller_update_params_tap_away_alt_lookups %>, <%= current_user_object %>, <%= datetime_fields_list %>) <% if @object_owner_sym && eval("#{class_name}.reflect_on_association(:#{@object_owner_sym})").class == ActiveRecord::Reflection::BelongsToReflection %>
101
+ modified_params = modify_date_inputs_on_params(<%= singular_name %>_params.dup, <%= current_user_object %>, <%= datetime_fields_list %>) <% if @object_owner_sym && eval("#{class_name}.reflect_on_association(:#{@object_owner_sym})").class == ActiveRecord::Reflection::BelongsToReflection %>
105
102
  modified_params = modified_params.merge(<%= @object_owner_sym %>: <%= @object_owner_eval %>) <% elsif @object_owner_optional && any_nested? %>
106
103
  modified_params = modified_params.merge(<%= @object_owner_name %> ? {<%= @object_owner_sym %>: <%= @object_owner_eval %>} : {}) <% elsif !@object_owner_eval.empty? %>
107
- modified_params = modified_params.merge(<%= @object_owner_eval %>) <% end %><% if !merge_lookups.empty? %>
108
- modified_params = modified_params.merge(<%= merge_lookups %>)
109
- <% end %>
104
+ modified_params = modified_params.merge(<%= @object_owner_eval %>) <% end %>
110
105
 
111
106
  <% if @hawk_keys.any? %>
112
107
  modified_params = hawk_params({<%= hawk_to_ruby %>}, modified_params)<% end %>
@@ -151,31 +146,19 @@ class <%= controller_class_name %> < <%= controller_descends_from %>
151
146
  <% end %><% if @build_update_action %> def update
152
147
  flash[:notice] = +''
153
148
  flash[:alert] = nil
154
- <% if @alt_lookups.filter{|key,d| ! @update_show_only.include?(key.to_sym) }.any? %><%= @alt_lookups.filter{|key,d| ! @update_show_only.include?(key.to_sym) }.collect{|key, data|
155
- " #{data[:assoc].downcase} = #{data[:assoc]}.#{data[:with_create] ? "find_or_create_by" : "find_by"}(#{data[:lookup_as]}: #{ singular_name }_params[:__lookup_#{data[:lookup_as]}])\n"
156
- }.join("\n") %><% end %> <% merge_lookups = @alt_lookups.filter{|key,d| ! @update_show_only.include?(key.to_sym) }.collect{|key, data| "#{key.gsub("_id", "")}: #{key.gsub("_id", "")}" }.join(",") %>
157
149
  <% @magic_buttons.each do |button| %>
158
150
  if <%= singular_name %>_params[:__<%= button %>]
159
151
  @<%= singular_name %>.<%= button %>!
160
152
  flash[:notice] << "<% singular %> <%= button.titlecase %>."
161
153
  end
162
154
  <% end %>
163
- modified_params = modify_date_inputs_on_params(<% if @update_show_only %>update_<% end %><%= singular_name %>_params.dup<%= controller_update_params_tap_away_alt_lookups %><%= controller_update_params_tap_away_magic_buttons %>, <%= current_user_object %>, <%= datetime_fields_list %>) <% if @object_owner_sym && eval("#{class_name}.reflect_on_association(:#{@object_owner_sym})").class == ActiveRecord::Reflection::BelongsToReflection %>
155
+ modified_params = modify_date_inputs_on_params(<% if @update_show_only %>update_<% end %><%= singular_name %>_params.dup<%= controller_update_params_tap_away_magic_buttons %>, <%= current_user_object %>, <%= datetime_fields_list %>) <% if @object_owner_sym && eval("#{class_name}.reflect_on_association(:#{@object_owner_sym})").class == ActiveRecord::Reflection::BelongsToReflection %>
164
156
  modified_params = modified_params.merge(<%= @object_owner_sym %>: <%= @object_owner_eval %>) <% elsif @object_owner_optional && any_nested? %>
165
157
  modified_params = modified_params.merge(<%= @object_owner_name %> ? {<%= @object_owner_sym %>: <%= @object_owner_eval %>} : {}) <% elsif ! @object_owner_eval.empty? && !@self_auth%>
166
- modified_params = modified_params.merge(<%= @object_owner_eval %>) <% end %><% if !merge_lookups.empty? %>
167
- modified_params = modified_params.merge(<%= merge_lookups %>)
168
- <% end %>
158
+ modified_params = modified_params.merge(<%= @object_owner_eval %>) <% end %>
169
159
 
170
160
  <% if @hawk_keys.any? %> modified_params = hawk_params({<%= hawk_to_ruby %>}, modified_params)<% end %>
171
- <% if @alt_lookups.any? %><%= @alt_lookups.collect{|key, data|
172
- unless @factory_creation.include?("#{data[:assoc].downcase} = ")
173
- " #{data[:assoc].downcase} = #{data[:assoc]}.#{data[:with_create] ? "find_or_create_by" : "find_by"}(#{data[:lookup_as]}: #{ singular_name }_params[:__lookup_#{data[:lookup_as]}])\n"
174
- end
175
- }.join("/n") %><% end %><% if (@update_alt_lookups).any? %>
176
- <%= @update_alt_lookups.collect{|key, data|
177
- " @#{ singular_name }.#{key.gsub("_id", "")} = #{key.gsub("_id", "")}"
178
- }.join("/n") %><% end %><%= controller_attachment_orig_filename_pickup_syntax %>
161
+ <%= controller_attachment_orig_filename_pickup_syntax %>
179
162
  <% if @pundit %>
180
163
  if @<%= singular_name %>.attributes = modified_params
181
164
  authorize @<%= singular_name %>
@@ -0,0 +1,14 @@
1
+ class <%= ((@namespace.titleize.gsub(" ", "") + "::" if @namespace) || "") + @plural.titleize.gsub(" ", "") + "TypeaheadController" %> < <%= controller_descends_from %>
2
+ # regenerate this controller with
3
+ <% if defined?(RuboCop) %># rubocop:disable Layout/LineLength
4
+ <% end %># <%= regenerate_me_code %><% if defined?(RuboCop) %>
5
+ # rubocop:enable Layout/LineLength <% end %>
6
+
7
+ def index
8
+ query = params[:query]
9
+
10
+ @<%= @plural %> = <%= @singular.titleize.gsub(" ", "") %>.where("<%= @search_by.collect{|search| "LOWER(#{search}) LIKE ?" }.join(" OR ") %>", <%= @search_by.collect{|search| "\"%\#{query.downcase}%\"" }.join(", ") %>).limit(10)
11
+
12
+ render layout: false
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ <li class="search__result" >
2
+ <span class="search-result-item" data-id="<\%= <%= @singular %>.id %>" data-value="<\%= <%= @singular %>.name %>">
3
+ <\%= <%= @singular %>.name %>
4
+ </span>
5
+ </li>
@@ -0,0 +1,13 @@
1
+
2
+ <div class="typeahead-results__<%= @plural %>"
3
+ data-controller="typeahead-results"
4
+ data-typeahead-results-typeahead-outlet=".typeahead--book--<%= @singular %>_id"
5
+ data-typeahead-results-current-class="search__result--current" >
6
+ <ul class="search__results" data-typeahead-results-target="result">
7
+ <\% if @<%= @plural %>.any? %>
8
+ <\%= render partial: "<%= @singular %>", collection: @<%= @plural %> %>
9
+ <\% else %>
10
+ <li class="search__result search__result--empty">No results</li>
11
+ <\% end %>
12
+ </ul>
13
+ </div>
@@ -0,0 +1,46 @@
1
+ .typeahead {
2
+
3
+ }
4
+
5
+ .search__input {
6
+ border: 1px solid gray;
7
+ border-radius: 2px;
8
+ padding: 10px;
9
+ font-size: 16px;
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ [data-typeahead-target="results"] {
14
+ position: absolute;
15
+ }
16
+
17
+ .search__results {
18
+ border: 1px solid gray;
19
+ border-radius: 2px;
20
+ border-top: none;
21
+ margin: 0;
22
+ padding: 0;
23
+ list-style-type: none;
24
+ top: -1px;
25
+ }
26
+
27
+ .search__result {
28
+ span.search-result-item {
29
+ text-decoration: none;
30
+ color: black;
31
+ padding: 10px;
32
+ display: block;
33
+ cursor: pointer;
34
+ &:hover {
35
+ background: #86b7fe;
36
+ }
37
+ }
38
+ }
39
+
40
+ .search__result--current, .search__result:hover {
41
+ background: #e0e0e0;
42
+ }
43
+
44
+ .search__result--empty {
45
+ padding: 10px;
46
+ }
@@ -0,0 +1,61 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = [ "query", "results", "hiddenFormValue" ]
5
+ static values = { url: String }
6
+ static outlets = [ "typeahead-results" ]
7
+
8
+ disconnect() {
9
+ this.reset()
10
+ }
11
+
12
+ fetchResults() {
13
+ if(this.query == "") {
14
+ this.reset()
15
+ return
16
+ }
17
+
18
+ if(this.query == this.previousQuery) {
19
+ return
20
+ }
21
+ this.previousQuery = this.query
22
+
23
+ const url = new URL(this.urlValue)
24
+ url.searchParams.append("query", this.query)
25
+
26
+ this.abortPreviousFetchRequest()
27
+
28
+ this.abortController = new AbortController()
29
+
30
+ fetch(url, { signal: this.abortController.signal })
31
+ .then(response => response.text())
32
+ .then(html => {
33
+ this.resultsTarget.innerHTML = html
34
+ })
35
+ .catch(() => {})
36
+ }
37
+
38
+ navigateResults(event) {
39
+ if(this.hasSearchResultsOutlet) {
40
+ this.searchResultsOutlet.navigateResults(event)
41
+ }
42
+ }
43
+
44
+ // private
45
+
46
+ reset() {
47
+ this.resultsTarget.innerHTML = ""
48
+ this.queryTarget.value = ""
49
+ this.previousQuery = null
50
+ }
51
+
52
+ abortPreviousFetchRequest() {
53
+ if(this.abortController) {
54
+ this.abortController.abort()
55
+ }
56
+ }
57
+
58
+ get query() {
59
+ return this.queryTarget.value
60
+ }
61
+ }
@@ -0,0 +1,87 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ const upKey = 38
4
+ const downKey = 40
5
+ const enterKey = 13
6
+ const navigationKeys = [upKey, downKey, enterKey]
7
+
8
+ export default class extends Controller {
9
+ static classes = [ "current" ]
10
+ static targets = [ "result" ]
11
+ static outlets = [ "typeahead" ]
12
+
13
+ connect() {
14
+ this.currentResultIndex = 0
15
+
16
+ const allElements = this.resultTarget.querySelectorAll(".search-result-item");
17
+
18
+ allElements.forEach((element, index) => {
19
+ element.addEventListener("click", () => {
20
+ // Call the searchItemClicked member function when the element is clicked
21
+ this.searchItemClicked(element, index);
22
+ });
23
+ })
24
+ this.selectCurrentResult()
25
+ }
26
+
27
+ searchItemClicked(element, index) {
28
+ const result_value = element.dataset.value;
29
+ const result_id = element.dataset.id;
30
+
31
+ // how to pass this to the search controller, set the field value and clear out the search
32
+ console.log("search item clicked...", result_value, result_id)
33
+
34
+ this.typeaheadOutlets.forEach(outlet => {
35
+ outlet.hiddenFormValueTarget.value = result_id;
36
+ outlet.queryTarget.value = result_value;
37
+ })
38
+
39
+ this.resultTarget.innerHTML = "";
40
+ }
41
+
42
+ navigateResults(event) {
43
+ if(!navigationKeys.includes(event.keyCode)) {
44
+ return
45
+ }
46
+
47
+ event.preventDefault()
48
+
49
+ switch(event.keyCode) {
50
+ case downKey:
51
+ this.selectNextResult()
52
+ break;
53
+ case upKey:
54
+ this.selectPreviousResult()
55
+ break;
56
+ case enterKey:
57
+ this.goToSelectedResult()
58
+ break;
59
+ }
60
+ }
61
+
62
+ // private
63
+
64
+ selectCurrentResult() {
65
+ this.resultTargets.forEach((element, index) => {
66
+ element.classList.toggle(this.currentClass, index == this.currentResultIndex)
67
+ })
68
+ }
69
+
70
+ selectNextResult() {
71
+ if(this.currentResultIndex < this.resultTargets.length - 1) {
72
+ this.currentResultIndex++
73
+ this.selectCurrentResult()
74
+ }
75
+ }
76
+
77
+ selectPreviousResult() {
78
+ if(this.currentResultIndex > 0) {
79
+ this.currentResultIndex--
80
+ this.selectCurrentResult()
81
+ }
82
+ }
83
+
84
+ goToSelectedResult() {
85
+ this.resultTargets[this.currentResultIndex].firstElementChild.click()
86
+ }
87
+ }
@@ -0,0 +1,105 @@
1
+ module HotGlue
2
+ class TypeaheadGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('templates', __dir__)
4
+ class_option :namespace, type: :string, default: nil
5
+ class_option :search_by, type: :string, default: nil
6
+
7
+ def filepath_prefix
8
+ # todo: inject the context
9
+ 'spec/dummy/' if $INTERNAL_SPECS
10
+ end
11
+
12
+
13
+ def initialize(*meta_args)
14
+ super
15
+ begin
16
+ @the_object = eval(meta_args[0][0])
17
+ rescue StandardError => e
18
+ message = "*** Oops: It looks like there is no object for #{meta_args[0][0]}. Please define the object + database table first."
19
+ puts message
20
+ raise(HotGlue::Error, message)
21
+ end
22
+
23
+ @singular = args.first.tableize.singularize
24
+
25
+ @plural = args.first.tableize.pluralize
26
+ @namespace = options['namespace']
27
+
28
+ if options['search_by']
29
+ @search_by = options['search_by'].split(",")
30
+ else
31
+ # todo: read the fields off the table
32
+
33
+ eligible_columns = @the_object.columns.filter{ |column|
34
+ column.sql_type == "character varying"
35
+ }
36
+ columns = eligible_columns.map(&:name).map(&:to_sym).reject { |field| [:id, :created_at, :updated_at].include?(field) }
37
+ @search_by = columns.collect(&:to_s)
38
+
39
+ end
40
+ @meta_args = meta_args
41
+ dest_file = "#{'spec/dummy/' if $INTERNAL_SPECS}app/controllers/#{@namespace ? @namespace + "/" : ""}#{@plural }_typeahead_controller.rb"
42
+ template "typeahead_controller.rb.erb", dest_file
43
+
44
+
45
+ dirname = "app/views/#{@namespace ? @namespace + "/" : ""}#{@plural}_typeahead"
46
+
47
+ if !Dir.exist?(dirname)
48
+ Dir.mkdir dirname
49
+ end
50
+ {"typeahead_views/_thing.html.erb" => "_#{@singular}.html.erb",
51
+ "typeahead_views/index.html.erb" => "index.html.erb"}.each do |source_filename, dest_filename|
52
+
53
+ dest_filepath = File.join("#{'spec/dummy/' if $INTERNAL_SPECS}app/views#{namespace_with_dash}",
54
+ "#{@plural}_typeahead", dest_filename)
55
+
56
+ template source_filename, dest_filepath
57
+ text = File.read(dest_filepath)
58
+ text.gsub!('<\\%=', '<%=' )
59
+ text.gsub!('<\\%', '<%' )
60
+ File.open(dest_filepath, "w") { |f| f.write text }
61
+ end
62
+
63
+
64
+ puts ""
65
+ puts "be sure to add this your config/routes.rb file:"
66
+ puts ""
67
+ if @namespace
68
+ puts "namespace :#{@namespace} do"
69
+ end
70
+ puts " resources :#{@plural}_typeahead, only: [:index]"
71
+ if @namespace
72
+ puts "end"
73
+ end
74
+ puts ""
75
+
76
+ end
77
+
78
+
79
+ def namespace_with_dash
80
+ if @namespace
81
+ "/#{@namespace}"
82
+ else
83
+ ""
84
+ end
85
+ end
86
+
87
+ def filepath_prefix
88
+ 'spec/dummy/' if $INTERNAL_SPECS
89
+ end
90
+
91
+ def controller_descends_from
92
+ if defined?(@namespace.titlecase.gsub(" ", "") + "::BaseController")
93
+ @namespace.titlecase.gsub(" ", "") + "::BaseController"
94
+ else
95
+ "ApplicationController"
96
+ end
97
+ end
98
+
99
+ def regenerate_me_code
100
+ "bin/rails generate hot_glue:typeahead #{ @meta_args[0][0] } #{@meta_args[1].collect { |x| x.gsub(/\s*=\s*([\S\s]+)/, '=\'\1\'') }.join(" ")}"
101
+ end
102
+ end
103
+
104
+
105
+ end
@@ -0,0 +1,40 @@
1
+ module HotGlue
2
+ class TypeaheadInstallGenerator < Rails::Generators::Base
3
+ source_root File.expand_path('templates', __dir__)
4
+ class_option :namespace, type: :string, default: nil
5
+
6
+ def filepath_prefix
7
+ # todo: inject the context
8
+ 'spec/dummy/' if $INTERNAL_SPECS
9
+ end
10
+
11
+
12
+ def initialize(*args) #:nodoc:
13
+ super
14
+
15
+ copy_file "typeahead_views/typeahead.scss", "#{'spec/dummy/' if $INTERNAL_SPECS}app/assets/stylesheets/typeahead.scss"
16
+
17
+
18
+ try = ["application.scss", "application.bootstrap.scss"]
19
+ try.each do |filename|
20
+ main_scss_file = "#{'spec/dummy/' if $INTERNAL_SPECS}app/assets/stylesheets/#{filename}"
21
+ if File.exist?(main_scss_file)
22
+ insert_into_file main_scss_file do
23
+ "@import 'typeahead';\n"
24
+ end
25
+ puts "Inserted @import 'typeahead'; into #{main_scss_file}"
26
+ break
27
+ else
28
+ # puts "Could not find #{main_scss_file}. Please add the following line to your main scss file: @import 'typeahead';"
29
+ end
30
+ end
31
+ copy_file "typeahead_views/typeahead_controller.js", "#{'spec/dummy/' if $INTERNAL_SPECS}app/javascript/controllers/typeahead_controller.js"
32
+ copy_file "typeahead_views/typeahead_results_controller.js", "#{'spec/dummy/' if $INTERNAL_SPECS}app/javascript/controllers/typeahead_results_controller.js"
33
+
34
+ system("bin/rails stimulus:manifest:update")
35
+ end
36
+ end
37
+ end
38
+
39
+
40
+
@@ -1,5 +1,5 @@
1
1
  module HotGlue
2
2
  class Version
3
- CURRENT = '0.5.26'
3
+ CURRENT = '0.6.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hot-glue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.26
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Fleetwood-Boldt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-23 00:00:00.000000000 Z
11
+ date: 2023-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -132,6 +132,14 @@ files:
132
132
  - lib/generators/hot_glue/templates/themes/hotglue_scaffold_like_bootstrap.scss
133
133
  - lib/generators/hot_glue/templates/themes/hotglue_scaffold_like_los_gatos.scss
134
134
  - lib/generators/hot_glue/templates/themes/hotglue_scaffold_like_mountain_view.scss
135
+ - lib/generators/hot_glue/templates/typeahead_controller.rb.erb
136
+ - lib/generators/hot_glue/templates/typeahead_views/_thing.html.erb
137
+ - lib/generators/hot_glue/templates/typeahead_views/index.html.erb
138
+ - lib/generators/hot_glue/templates/typeahead_views/typeahead.scss
139
+ - lib/generators/hot_glue/templates/typeahead_views/typeahead_controller.js
140
+ - lib/generators/hot_glue/templates/typeahead_views/typeahead_results_controller.js
141
+ - lib/generators/hot_glue/typeahead_generator.rb
142
+ - lib/generators/hot_glue/typeahead_install_generator.rb
135
143
  - lib/hot-glue.rb
136
144
  - lib/hotglue/engine.rb
137
145
  - lib/hotglue/version.rb