cdek 0.3.12 → 0.3.14

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: 5dfa6bb78c45fc7a0bf40bf04cdf59999b7598766cf0ef5c3b086b699db45f38
4
- data.tar.gz: c9f43b02cfc8b5314ad3b4338f8786e319216b0e72878e2f82a817f4cbca76f9
3
+ metadata.gz: 876ccf8ab0ab45c5d2ea73d3a2abd8d396e5abcedf8c30d7fbdd3c18fbaf9f00
4
+ data.tar.gz: af5b8ea84efcfd064cf7a265405ecc5d0460bb338ec12f4fc25d3c4ac2e885e1
5
5
  SHA512:
6
- metadata.gz: 8e532a826e6bcacae86aaf2f1f4ee23f14ae1358889c344a632cc76107deb7e359d16b61983921a7fdc2f4b7e274f262d7fa10f010af9b2e276dec5c6af96267
7
- data.tar.gz: ed305863d0c728ba013f3f6b7e363e54e6e973dd0e67d76c28468750f685157af106a6a13a2aa7cfa43e8d6eff0f6d389f4f0701c51680f368f35a3562572780
6
+ metadata.gz: 6da66c243aeba6ee2c00c6f151b1873c9b5c47924eb89cf5f9a6031d9edc002d04320a7f71b419fcad64190c25c3968efcf1161538f228798a8953c360bf4502
7
+ data.tar.gz: d49a1accd14e41e2b4cf199e3f6a42892a61a32400e3f683519b12aa81a0b51d1d84ef47489bd35b47261aa3fc30c3da75df2e885604c436125d14e7f9f552ce
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.14
4
+
5
+ * Шаблон генератора `cdek_widget_controller.js` теперь держит ровно один
6
+ живой инстанс виджета на странице: предыдущий уничтожается синхронно
7
+ перед монтированием нового. `@cdek-it/widget` — Vue-приложение с
8
+ глобальным стором, поэтому два одновременно живых инстанса делили стор
9
+ и конфликтовали — при смене города (пересоздание модалки) старый инстанс
10
+ перезапрашивал офисы и пере-центрировал карту на прежний город (точки
11
+ выбранного города мелькали и пропадали). Добавлены module-level
12
+ `_activeWidget` и `destroyWidget()`; `disconnect` и `_mountWidget`
13
+ переведены на них. Хост-приложениям, сгенерировавшим контроллер на
14
+ ≤0.3.13, нужно перегенерировать его (`bin/rails generate cdek:install`)
15
+ или внести правку вручную.
16
+
17
+ ## 0.3.13
18
+
19
+ * Виджет ПВЗ больше не откатывает центр карты на Москву при выборе
20
+ не-московского города. `defaultLocation` передавался строкой-названием,
21
+ центр ставился асинхронным геокодингом и при сбое оставался дефолт
22
+ [37.62,55.75]. `CityResolver` теперь из одного кэшированного
23
+ `/location/cities` отдаёт и `code`, и координаты `[lng,lat]`;
24
+ `cdek_widget_tag` прокидывает `default_coords`, JS-контроллер использует
25
+ синхронную ветку без геокодинга. При отсутствии координат — fallback на
26
+ строку (нулевая регрессия). Перегенерировать контроллер:
27
+ `bin/rails generate cdek:install`.
28
+
3
29
  ## 0.3.9
4
30
 
5
31
  * В `cdek.gemspec` добавлена development-зависимость `rake`, потому что
@@ -51,6 +51,7 @@ module Cdek
51
51
  data = cdek_widget_data(
52
52
  api_key: api_key.presence || ENV["YANDEX_MAPS_API_KEY"].to_s,
53
53
  default_city: default_city,
54
+ default_coords: cdek_default_coords(default_city),
54
55
  sender_city: sender_city,
55
56
  sender_city_code: sender_city_code.to_s,
56
57
  goods_json: JSON.generate(goods_payload),
@@ -91,7 +92,7 @@ module Cdek
91
92
 
92
93
  # Хэш data-* атрибутов для Stimulus-контроллера cdek-widget.
93
94
  # Ключи в snake_case — Rails сам конвертит "_" в "-" в HTML.
94
- def cdek_widget_data(api_key:, default_city:, sender_city:, sender_city_code:,
95
+ def cdek_widget_data(api_key:, default_city:, default_coords:, sender_city:, sender_city_code:,
95
96
  goods_json:,
96
97
  service_path:, script_url:, modal_id:,
97
98
  field_code:, field_name:, field_address:, field_city_code:,
@@ -102,6 +103,7 @@ module Cdek
102
103
  cdek_widget_script_url_value: script_url,
103
104
  cdek_widget_api_key_value: api_key,
104
105
  cdek_widget_default_location_value: default_city,
106
+ cdek_widget_default_coords_value: default_coords,
105
107
  cdek_widget_sender_city_value: sender_city,
106
108
  cdek_widget_sender_city_code_value: sender_city_code,
107
109
  cdek_widget_goods_value: goods_json,
@@ -115,6 +117,21 @@ module Cdek
115
117
  }
116
118
  end
117
119
 
120
+ # Координаты города «по умолчанию» строкой "долгота,широта" для data-атрибута
121
+ # (или "" если город пуст / не нашёлся / CDEK недоступен). JS-контроллер
122
+ # передаёт их виджету как defaultLocation = [lng, lat]; при пустой строке
123
+ # он откатывается на defaultLocation = название города (прежнее поведение).
124
+ def cdek_default_coords(city_name)
125
+ coords =
126
+ begin
127
+ Cdek.city_coordinates(city_name)
128
+ rescue Cdek::Error
129
+ nil
130
+ end
131
+
132
+ coords.is_a?(Array) && coords.length == 2 ? coords.join(",") : ""
133
+ end
134
+
118
135
  # Путь до UMD-бандла виджета, вшитого в гем. Используется JS-контроллером
119
136
  # для динамической загрузки скрипта по требованию.
120
137
  def cdek_widget_asset_path
@@ -1,18 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cdek
4
- # Resolves a human-readable city name to a CDEK city code.
4
+ # Resolves a human-readable city name to a CDEK city record — both the CDEK
5
+ # city code and the city coordinates — in one API-backed, cached lookup.
5
6
  #
6
7
  # This is intentionally small and API-backed: host applications can pass
7
8
  # user-entered city names to the widget proxy without duplicating lookup
8
- # logic or loading all delivery points for the whole country.
9
+ # logic or loading all delivery points for the whole country. The widget
10
+ # proxy uses #code to filter offices, while the widget helper uses
11
+ # #coordinates to center the map on the chosen city (passing coordinates to
12
+ # the JS widget instead of a name avoids a flaky async geocode that could
13
+ # otherwise drop the map back to its built-in default center).
9
14
  class CityResolver
10
15
  DEFAULT_COUNTRY_CODES = "RU"
11
16
  CACHE_EXPIRES_IN = 43_200
12
17
 
13
18
  class << self
14
19
  def call(name, **options)
15
- new(name, **options).call
20
+ new(name, **options).code
21
+ end
22
+
23
+ def coordinates(name, **options)
24
+ new(name, **options).coordinates
16
25
  end
17
26
  end
18
27
 
@@ -23,27 +32,45 @@ module Cdek
23
32
  @cache = cache
24
33
  end
25
34
 
26
- def call
27
- normalized_name.empty? ? nil : cached_city_code
35
+ # CDEK city code or nil when the city can't be resolved.
36
+ def code
37
+ city = cached_city
38
+ city.is_a?(Hash) ? (city["code"] || city[:code]) : nil
39
+ end
40
+
41
+ # [longitude, latitude] (floats) or nil when coordinates are unknown.
42
+ # The order matches what the CDEK JS widget expects ([lng, lat]).
43
+ def coordinates
44
+ city = cached_city
45
+ lng = city_coordinate(city, "longitude")
46
+ lat = city_coordinate(city, "latitude")
47
+
48
+ (lng && lat) ? [lng, lat] : nil
28
49
  end
29
50
 
30
51
  private
31
52
 
32
53
  attr_reader :name, :client, :country_codes, :cache
33
54
 
34
- def cached_city_code
35
- if cache
36
- cache.fetch(cache_key, expires_in: CACHE_EXPIRES_IN) { fetch_city_code }
55
+ def cached_city
56
+ if normalized_name.empty?
57
+ nil
58
+ elsif cache
59
+ cache.fetch(cache_key, expires_in: CACHE_EXPIRES_IN) { fetch_city }
37
60
  else
38
- fetch_city_code
61
+ fetch_city
39
62
  end
40
63
  end
41
64
 
42
- def fetch_city_code
43
- city = Cdek.locations(client).find_city(normalized_name, country_codes: country_codes)
65
+ def fetch_city
66
+ Cdek.locations(client).find_city(normalized_name, country_codes: country_codes)
67
+ end
44
68
 
69
+ def city_coordinate(city, key)
45
70
  if city.is_a?(Hash)
46
- city["code"] || city[:code]
71
+ raw = city.key?(key) ? city[key] : city[key.to_sym]
72
+ number = Float(raw, exception: false)
73
+ number&.finite? ? number : nil
47
74
  end
48
75
  end
49
76
 
@@ -52,7 +79,7 @@ module Cdek
52
79
  end
53
80
 
54
81
  def cache_key
55
- ["cdek", "city_code", country_codes, normalized_name.downcase].join(":")
82
+ ["cdek", "city", country_codes, normalized_name.downcase].join(":")
56
83
  end
57
84
 
58
85
  def default_cache
data/lib/cdek/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cdek
4
- VERSION = "0.3.12"
4
+ VERSION = "0.3.14"
5
5
  end
data/lib/cdek.rb CHANGED
@@ -68,6 +68,13 @@ module Cdek
68
68
  CityResolver.call(name, **options)
69
69
  end
70
70
 
71
+ # Координаты [долгота, широта] города по пользовательскому названию
72
+ # (или nil, если город/координаты не нашлись). Нужны виджету, чтобы
73
+ # центрировать карту по выбранному городу координатами, а не названием.
74
+ def city_coordinates(name, **options)
75
+ CityResolver.coordinates(name, **options)
76
+ end
77
+
71
78
  # Нормализованные подсказки городов для autocomplete в хост-приложении.
72
79
  def city_suggestions(query, **options)
73
80
  CitySuggestions.call(query, **options)
@@ -28,6 +28,24 @@ import { Controller } from "@hotwired/stimulus"
28
28
 
29
29
  let _scriptPromise = null
30
30
 
31
+ // @cdek-it/widget@3 — Vue-приложение с ГЛОБАЛЬНЫМ стором (singleton). Два
32
+ // живых инстанса делят один стор и конфликтуют: монтаж нового перенастраивает
33
+ // общий стор, и старый перезапрашивает офисы и пере-центрирует карту на
34
+ // прежний город (в server-логах — повторные widget_service по старому городу
35
+ // сразу после нового). Поэтому держим ровно один живой виджет на странице и
36
+ // гасим предыдущий ПЕРЕД монтажом нового. destroy() у виджета бросает, если он
37
+ // ещё не домонтировался, — глушим, ссылку всё равно чистим.
38
+ let _activeWidget = null
39
+
40
+ function destroyWidget(widget) {
41
+ if (!widget) return
42
+ try {
43
+ if (typeof widget.destroy === "function") widget.destroy()
44
+ else if (typeof widget.close === "function") widget.close()
45
+ } catch (_) { /* виджет мог не домонтироваться — это ок */ }
46
+ if (_activeWidget === widget) _activeWidget = null
47
+ }
48
+
31
49
  function ensureWidgetScript(url) {
32
50
  if (typeof window.CDEKWidget !== "undefined") {
33
51
  return Promise.resolve()
@@ -77,6 +95,7 @@ export default class extends Controller {
77
95
  scriptUrl: String,
78
96
  apiKey: String,
79
97
  defaultLocation: { type: String, default: "Москва" },
98
+ defaultCoords: { type: String, default: "" },
80
99
  senderCity: { type: String, default: "Москва" },
81
100
  senderCityCode: { type: String, default: "" },
82
101
  goods: { type: String, default: "" },
@@ -100,13 +119,8 @@ export default class extends Controller {
100
119
  window.clearTimeout(this._mountTimer)
101
120
  this._mountTimer = null
102
121
  }
103
- if (this._widget) {
104
- try {
105
- if (typeof this._widget.destroy === "function") this._widget.destroy()
106
- else if (typeof this._widget.close === "function") this._widget.close()
107
- } catch (_) { /* no-op */ }
108
- this._widget = null
109
- }
122
+ destroyWidget(this._widget)
123
+ this._widget = null
110
124
  }
111
125
 
112
126
  _mountWhenReady() {
@@ -138,6 +152,22 @@ export default class extends Controller {
138
152
  return url.pathname + url.search
139
153
  }
140
154
 
155
+ // Координаты выбранного города ([lng, lat]) приходят из data-cdek-widget-default-coords-value.
156
+ // Передаём их виджету как defaultLocation — тогда он центрирует карту по
157
+ // координатам синхронно и не делает асинхронный геокодинг названия, который
158
+ // при сбое оставляет карту на встроенном дефолтном центре (Москве). Если
159
+ // координат нет (город не нашёлся / пусто) — откатываемся на название города.
160
+ _resolveDefaultLocation() {
161
+ const raw = this.defaultCoordsValue.trim()
162
+ const parts = raw === "" ? [] : raw.split(",")
163
+ const lng = Number(parts[0])
164
+ const lat = Number(parts[1])
165
+
166
+ return (parts.length === 2 && Number.isFinite(lng) && Number.isFinite(lat))
167
+ ? [lng, lat]
168
+ : this.defaultLocationValue
169
+ }
170
+
141
171
  _mountWidget() {
142
172
  if (!this.hasRootTarget) return
143
173
  if (typeof window.CDEKWidget !== "function") {
@@ -164,7 +194,7 @@ export default class extends Controller {
164
194
  root: this.rootTarget.id,
165
195
  servicePath: this._servicePath(),
166
196
  apiKey: this.apiKeyValue,
167
- defaultLocation: this.defaultLocationValue,
197
+ defaultLocation: this._resolveDefaultLocation(),
168
198
  from: from,
169
199
  hideDeliveryOptions: { door: true, office: false },
170
200
  lang: "rus",
@@ -176,8 +206,13 @@ export default class extends Controller {
176
206
  widgetOptions.goods = goods
177
207
  }
178
208
 
209
+ // Гасим любой предыдущий инстанс ДО создания нового — иначе два виджета
210
+ // дерутся за глобальный стор (старый город перезапрашивается, карта прыгает).
211
+ destroyWidget(_activeWidget)
212
+
179
213
  try {
180
214
  this._widget = new window.CDEKWidget(widgetOptions)
215
+ _activeWidget = this._widget
181
216
  } catch (err) {
182
217
  this._showError(err && err.message ? err.message : "ошибка инициализации виджета")
183
218
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cdek
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.12
4
+ version: 0.3.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Korolyov