formatic 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c52f37c072f5cbebbfa6718393edae33f687911b3e426db8a97a3cd6b5a0c72
4
- data.tar.gz: bea3f5680a56ba9b447e7fa925eb60aaa809a756d0c1b9d8d7b573b967752f03
3
+ metadata.gz: c770f8b2518ead02b5d2a7797068c3192ed9d264e22d59af830569eb07a6c0ef
4
+ data.tar.gz: ffc6912dea00ef5faf99fa815d7d3173d446fb07f312c160a80e6d29f9ec175c
5
5
  SHA512:
6
- metadata.gz: f86e74b0b5a1ca07c73d265672b6770ee543f1879e098498cc6a7cb64bf8d9b26972f5120b6c5fb0299242991640a47c671b94c3e002ab3addfb911cbaae3abe
7
- data.tar.gz: 4dd6d6b3102c55177e11ca879f0ffab0edce31cf7595914ffa0802746c6157da138833f138fcd8dca5ed793139998d8f1393588f7bd099a030123131815004c8
6
+ metadata.gz: eaf5adeb08637ff84743f4a0c26c694533af908ee64d716340f96478fed4cc2a93004bf73a5bb3c50d8be4b647e703ab7f5d9bafb93cad4e483473c60501760c
7
+ data.tar.gz: af465ca562212a64012756431fd96340d2e44c5d68a11ee4ede6a912f2e8d5627b853140b0f759d57d84f90f1d6722aab752496e47a386bd5c56132f7d5d9e0f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.6.1] 2026-10-26
2
+
3
+ - Proper i18n for Formatic::Search placeholders
4
+
5
+ ## [0.6.0] 2026-10-26
6
+
7
+ - Include `Formatic::Testing::Capybara::Search` in a Capybara RackTest class to call `search 'Acme Corp', from: 'Customer'`
8
+
9
+ ## [0.5.1] 2026-10-26
10
+
11
+ - Rack tests call `Formatic::Testing::Search.call` instead of including a Capybara module
12
+
1
13
  ## [0.5.0] 2026-10-26
2
14
 
3
15
  - Add `Formatic::Search`, a JSON-backed search field
data/README.md CHANGED
@@ -22,6 +22,11 @@ when its query still matches the input. An optional `with_clear` slot clears
22
22
  the current selection.
23
23
  An optional `with_add_record` slot stays hidden while searching and appears
24
24
  after a successful response, even when there are no matches.
25
+ The query input uses a field-specific `placeholder:` or `helpers.placeholder`
26
+ translation when present. Otherwise it combines the translated label with the
27
+ localized search prompt. English and German prompts are included; other locales
28
+ fall back to the label. Search fields prevent implicit form submission on Enter
29
+ by default; pass `prevent_submit_on_enter: false` to disable that behavior.
25
30
 
26
31
  ```ruby
27
32
  = render Formatic::Search.new(f:, attribute_name: :commander_id,
@@ -29,11 +34,24 @@ after a successful response, even when there are no matches.
29
34
  url: admin_commanders_path(format: :json))
30
35
  ```
31
36
 
32
- For Rack tests, include `Formatic::Testing::Capybara::Search` in a
33
- `Capybara::RackTest` test class. `select_record_from_search 'Bob', from: 'Users'`
34
- reads the field's URL and parameter names, requests matching JSON directly, and
37
+ For Rack tests, call `Formatic::Testing::Search.call(page:, query: 'Bob', from: 'Users')`.
38
+ It reads the field's URL and parameter names, requests matching JSON directly, and
35
39
  fills the hidden input with a result returned by that endpoint.
36
40
 
41
+ Capybara RackTest test classes can include the `search` helper instead:
42
+
43
+ ```ruby
44
+ require 'formatic/testing/capybara'
45
+
46
+ class CustomersTest < ActionDispatch::SystemTestCase
47
+ include Formatic::Testing::Capybara::Search
48
+
49
+ test 'select a customer' do
50
+ search 'Acme Corp', from: 'Customer'
51
+ end
52
+ end
53
+ ```
54
+
37
55
  ## Installation
38
56
 
39
57
  Install the gem and add to the application's Gemfile by executing:
@@ -49,18 +49,22 @@ module Formatic
49
49
  private
50
50
 
51
51
  def validate_attribute_name!
52
+ return unless validate_attribute_name?
53
+ return if f.object.respond_to?(attribute_name)
54
+
55
+ raise ArgumentError,
56
+ "attribute `#{attribute_name}` does not exist on #{f.object.class}."
57
+ end
58
+
59
+ def validate_attribute_name?
52
60
  # A manually passed value means `attribute_name` is just a param name,
53
61
  # not necessarily a method on the model.
54
- return if manual_value != :_fetch_from_record
55
- return if f.nil? || f.object.nil?
62
+ return false if manual_value != :_fetch_from_record
63
+ return false if f.nil? || f.object.nil?
56
64
 
57
65
  # Only enforce this for real ActiveModel/ActiveRecord records.
58
66
  # Custom objects (e.g. slugs) are not expected to respond to it.
59
- return unless f.object.respond_to?(:model_name)
60
- return if f.object.respond_to?(attribute_name)
61
-
62
- raise ArgumentError,
63
- "attribute `#{attribute_name}` does not exist on #{f.object.class}."
67
+ f.object.respond_to?(:model_name)
64
68
  end
65
69
 
66
70
  def attachments
@@ -77,19 +81,22 @@ module Formatic
77
81
  # Filepond doesn't work well with invalid form submits anyway.
78
82
  return if multiple
79
83
 
80
- attachments.map do |attachment|
81
- {
82
- source: attachment.signed_id,
83
- options: {
84
- type: 'local',
85
- file: {
86
- name: attachment.filename,
87
- size: attachment.byte_size,
88
- type: attachment.content_type
89
- }
90
- }
91
- }
92
- end.to_json
84
+ attachments.map { |attachment| entry_json(attachment) }.to_json
85
+ end
86
+
87
+ def entry_json(attachment)
88
+ {
89
+ source: attachment.signed_id,
90
+ options: { type: 'local', file: file_json(attachment) }
91
+ }
92
+ end
93
+
94
+ def file_json(attachment)
95
+ {
96
+ name: attachment.filename,
97
+ size: attachment.byte_size,
98
+ type: attachment.content_type
99
+ }
93
100
  end
94
101
 
95
102
  # The currently attached file, if there is exactly one.
@@ -9,6 +9,7 @@ module Formatic
9
9
  option :record_name, as: :manual_record_name, optional: true
10
10
  option :exclude_ids, default: -> { [] }
11
11
  option :placeholder, as: :manual_placeholder, optional: true
12
+ option :prevent_submit_on_enter, default: -> { true }
12
13
 
13
14
  renders_one :selected_record
14
15
  renders_one :add_record
@@ -24,7 +25,11 @@ module Formatic
24
25
  end
25
26
 
26
27
  def search_placeholder
27
- manual_placeholder.presence || wrapper.placeholder
28
+ return manual_placeholder if manual_placeholder.present?
29
+ return wrapper.placeholder if wrapper.placeholder.present?
30
+
31
+ translated_label = helpers.strip_tags(f.label(attribute_name))
32
+ ::I18n.t('formatic.search.placeholder', label: translated_label, default: translated_label)
28
33
  end
29
34
  end
30
35
  end
@@ -3,3 +3,6 @@ de:
3
3
 
4
4
  date:
5
5
  blank: Leer
6
+
7
+ search:
8
+ placeholder: '%{label} suchen...'
@@ -3,3 +3,6 @@ en:
3
3
 
4
4
  date:
5
5
  blank: Blank
6
+
7
+ search:
8
+ placeholder: Search %{label}...
@@ -1,92 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'calls'
3
+ require_relative 'search'
5
4
 
6
5
  module Formatic
7
6
  module Testing
8
7
  module Capybara
8
+ # Include in a Capybara RackTest test class to select Formatic search results.
9
+ #
10
+ # include Formatic::Testing::Capybara::Search
11
+ #
12
+ # search 'Acme Corp', from: 'Customer'
9
13
  module Search
10
- def select_record_from_search(query, from:)
11
- SearchSelector.call(page: page, query:, from:)
12
- end
13
-
14
- class SearchSelector
15
- include Calls
16
-
17
- class SelectionError < ::StandardError; end
18
-
19
- option :page
20
- option :query
21
- option :from
22
-
23
- def call
24
- ensure_rack_test_driver!
25
- query_field = page.find_field(from)
26
- component = search_component(query_field)
27
- record = matching_record(search_records(component), query)
28
-
29
- raise SelectionError, "No result matching #{query.inspect} from #{from.inspect}" unless record
30
-
31
- apply_selection(component, query_field, record)
32
- record
33
- end
34
-
35
- private
36
-
37
- def ensure_rack_test_driver!
38
- require 'capybara/rack_test/driver'
39
- return if page.driver.is_a?(::Capybara::RackTest::Driver)
40
-
41
- raise ArgumentError, 'select_record_from_search requires Capybara RackTest'
42
- end
43
-
44
- def search_component(query_field)
45
- query_field.find(:xpath, 'ancestor::div[contains(@class, "js-formatic-search")][1]')
46
- end
47
-
48
- def search_records(component)
49
- response = search_request(configured_value(component, 'resource'), search_params(component))
50
-
51
- raise SelectionError, "Search for #{from.inspect} returned HTTP #{response.status}" unless response.ok?
52
-
53
- JSON.parse(response.body).fetch('records')
54
- end
55
-
56
- def search_params(component)
57
- {
58
- query: query,
59
- record_name: configured_value(component, 'record-name'),
60
- attribute_name: configured_value(component, 'attribute-name'),
61
- exclude_ids: configured_value(component, 'exclude-ids')
62
- }
63
- end
64
-
65
- def configured_value(component, class_suffix)
66
- component.find(".js-formatic-search__#{class_suffix}", visible: :all).value
67
- end
68
-
69
- def search_request(url, params)
70
- browser = page.driver.browser
71
- browser.current_session.restore_state do
72
- browser.get(url, params, 'HTTP_ACCEPT' => 'application/json')
73
- end
74
- end
75
-
76
- def matching_record(records, query)
77
- records.find do |record|
78
- text = record.fetch('choice').gsub(/<[^>]*>/, ' ')
79
- text.downcase.include?(query.downcase)
80
- end
81
- end
82
-
83
- def apply_selection(component, query_field, record)
84
- component.find('.js-formatic-search__value', visible: :all).set(record.fetch('id').to_s)
85
- selected = component.find('.js-formatic-search__selected', visible: :all)
86
- selected.native.inner_html = record.fetch('selected')
87
- component.find('.js-formatic-search__results', visible: :all).native.inner_html = ''
88
- query_field.set('')
89
- end
14
+ def search(query, from:)
15
+ ::Formatic::Testing::Search.call(page:, query:, from:)
90
16
  end
91
17
  end
92
18
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'calls'
5
+
6
+ module Formatic
7
+ module Testing
8
+ # Selects a Formatic search result in a Rack test.
9
+ class Search
10
+ include Calls
11
+
12
+ class SelectionError < ::StandardError; end
13
+
14
+ option :page
15
+ option :query
16
+ option :from
17
+
18
+ def call
19
+ ensure_rack_test_driver!
20
+ query_field = page.find_field(from)
21
+ component = search_component(query_field)
22
+ record = matching_record(search_records(component), query)
23
+
24
+ raise SelectionError, "No result matching #{query.inspect} from #{from.inspect}" unless record
25
+
26
+ apply_selection(component, query_field, record)
27
+ record
28
+ end
29
+
30
+ private
31
+
32
+ def ensure_rack_test_driver!
33
+ require 'capybara/rack_test/driver'
34
+ return if page.driver.is_a?(::Capybara::RackTest::Driver)
35
+
36
+ raise ArgumentError, 'Formatic::Testing::Search requires Capybara RackTest'
37
+ end
38
+
39
+ def search_component(query_field)
40
+ query_field.find(:xpath, 'ancestor::div[contains(@class, "js-formatic-search")][1]')
41
+ end
42
+
43
+ def search_records(component)
44
+ response = search_request(configured_value(component, 'url'), search_params(component))
45
+
46
+ raise SelectionError, "Search for #{from.inspect} returned HTTP #{response.status}" unless response.ok?
47
+
48
+ JSON.parse(response.body).fetch('records')
49
+ end
50
+
51
+ def search_params(component)
52
+ {
53
+ query: query,
54
+ record_name: configured_value(component, 'record-name'),
55
+ attribute_name: configured_value(component, 'attribute-name'),
56
+ exclude_ids: configured_value(component, 'exclude-ids')
57
+ }
58
+ end
59
+
60
+ def configured_value(component, class_suffix)
61
+ component.find(".js-formatic-search__#{class_suffix}", visible: :all).value
62
+ end
63
+
64
+ def search_request(url, params)
65
+ browser = page.driver.browser
66
+ browser.current_session.restore_state do
67
+ browser.get(url, params, 'HTTP_ACCEPT' => 'application/json')
68
+ end
69
+ end
70
+
71
+ def matching_record(records, query)
72
+ records.find do |record|
73
+ text = record.fetch('choice').gsub(/<[^>]*>/, ' ')
74
+ text.downcase.include?(query.downcase)
75
+ end
76
+ end
77
+
78
+ def apply_selection(component, query_field, record)
79
+ component.find('.js-formatic-search__value', visible: :all).set(record.fetch('id').to_s)
80
+ selected = component.find('.js-formatic-search__selected', visible: :all)
81
+ selected.native.inner_html = record.fetch('selected')
82
+ component.find('.js-formatic-search__results', visible: :all).native.inner_html = ''
83
+ query_field.set('')
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Formatic
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: formatic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - halo
@@ -218,6 +218,7 @@ files:
218
218
  - lib/formatic/templates/select.rb
219
219
  - lib/formatic/templates/wrapper.rb
220
220
  - lib/formatic/testing/capybara.rb
221
+ - lib/formatic/testing/search.rb
221
222
  - lib/formatic/version.rb
222
223
  - lib/formatic/vscode_snippets.rb
223
224
  - lib/formatic/wrappers/alternative_attribute_name.rb
@@ -248,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
248
249
  - !ruby/object:Gem::Version
249
250
  version: '0'
250
251
  requirements: []
251
- rubygems_version: 4.0.6
252
+ rubygems_version: 4.0.20
252
253
  specification_version: 4
253
254
  summary: A set of ViewComponents for form elements
254
255
  test_files: []