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 +4 -4
- data/CHANGELOG.md +26 -0
- data/app/helpers/cdek/widget_helper.rb +18 -1
- data/lib/cdek/city_resolver.rb +40 -13
- data/lib/cdek/version.rb +1 -1
- data/lib/cdek.rb +7 -0
- data/lib/generators/cdek/install/templates/cdek_widget_controller.js +43 -8
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 876ccf8ab0ab45c5d2ea73d3a2abd8d396e5abcedf8c30d7fbdd3c18fbaf9f00
|
|
4
|
+
data.tar.gz: af5b8ea84efcfd064cf7a265405ecc5d0460bb338ec12f4fc25d3c4ac2e885e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/cdek/city_resolver.rb
CHANGED
|
@@ -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
|
|
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).
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
35
|
-
if
|
|
36
|
-
|
|
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
|
-
|
|
61
|
+
fetch_city
|
|
39
62
|
end
|
|
40
63
|
end
|
|
41
64
|
|
|
42
|
-
def
|
|
43
|
-
|
|
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[
|
|
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", "
|
|
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
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
|
-
|
|
104
|
-
|
|
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.
|
|
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
|
}
|