@andrey4emk/npm-app-front-b24 0.2.0 → 0.3.0

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.
package/ConfigLogs.vue CHANGED
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="config-logs">
3
- <h2>Уровни логирования</h2>
3
+ <h2 class="config-logs__title">Уровни логирования</h2>
4
4
 
5
5
  <div v-if="loading" class="config-logs__loading">Загрузка...</div>
6
6
 
@@ -47,10 +47,10 @@
47
47
  </table>
48
48
 
49
49
  <div class="config-logs__actions">
50
- <button @click="saveConfig" :disabled="saving">
50
+ <button @click="saveConfig" :disabled="loading || saving">
51
51
  {{ saving ? "Сохранение..." : "Сохранить" }}
52
52
  </button>
53
- <button @click="loadConfig" :disabled="loading">Обновить</button>
53
+ <button @click="loadConfig" :disabled="loading || saving">Обновить</button>
54
54
  </div>
55
55
 
56
56
  <div v-if="message" :class="['config-logs__message', messageType === 'ok' ? 'config-logs__message--success' : 'config-logs__message--error']">
@@ -113,34 +113,88 @@ function getUrl() {
113
113
  return `${base}/api/config/${props.configName}`;
114
114
  }
115
115
 
116
+ // Разбирает тело ответа: JSON — объект, любой другой Content-Type — null.
117
+ // Отдельная функция нужна, потому что сервер на неизвестный маршрут отвечает
118
+ // HTML-страницей с кодом 200, и слепой res.json() падает с невнятной ошибкой
119
+ async function parseJsonBody(res) {
120
+ const contentType = res.headers.get("content-type") || "";
121
+ if (!contentType.includes("application/json")) {
122
+ return null;
123
+ }
124
+ try {
125
+ return await res.json();
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ // Текст ошибки по HTTP-статусу. Если сервер прислал message — он попадает в текст
132
+ // при любом коде, включая 401/403 и 5xx: иначе причина отказа теряется
133
+ function httpErrorText(res, json) {
134
+ if (res.status === 401 || res.status === 403) {
135
+ return json?.message
136
+ ? `Нет доступа (${res.status}): ${json.message}`
137
+ : "Нет доступа, проверьте secretKey";
138
+ }
139
+ if (res.status >= 500) {
140
+ return json?.message
141
+ ? `Ошибка на сервере (${res.status}): ${json.message}`
142
+ : `Ошибка на сервере (${res.status})`;
143
+ }
144
+ const detail = json?.message ? `: ${json.message}` : "";
145
+ return `Ошибка запроса (${res.status})${detail}`;
146
+ }
147
+
148
+ // Раскладывает пришедший конфиг по реактивным уровням
149
+ function applyConfig(data) {
150
+ for (const level of levels) {
151
+ if (!data[level.key]) continue;
152
+ config[level.key].enabled = data[level.key].enabled ?? false;
153
+ config[level.key].json = data[level.key].json ?? true;
154
+ config[level.key].color = data[level.key].color ?? "";
155
+ }
156
+ }
157
+
116
158
  async function loadConfig() {
159
+ // Пока идёт загрузка или сохранение, повторный запуск запрещён:
160
+ // иначе ответы двух запросов затирают друг друга
161
+ if (loading.value || saving.value) return;
162
+
117
163
  loading.value = true;
118
164
  error.value = "";
119
165
  message.value = "";
120
166
 
121
167
  try {
122
168
  const res = await fetch(getUrl(), { headers: getHeaders() });
123
- const json = await res.json();
124
-
125
- if (json.status === "ok" && json.data) {
126
- for (const level of levels) {
127
- if (json.data[level.key]) {
128
- config[level.key].enabled = json.data[level.key].enabled ?? false;
129
- config[level.key].json = json.data[level.key].json ?? true;
130
- config[level.key].color = json.data[level.key].color ?? "";
131
- }
132
- }
133
- } else {
169
+ const json = await parseJsonBody(res);
170
+
171
+ if (!res.ok) {
172
+ error.value = httpErrorText(res, json);
173
+ return;
174
+ }
175
+ if (!json) {
176
+ error.value = "Неожиданный ответ сервера: тело не JSON. Проверьте apiBase";
177
+ return;
178
+ }
179
+ if (json.status !== "ok" || !json.data) {
134
180
  error.value = json.message || "Не удалось загрузить конфиг";
181
+ return;
135
182
  }
183
+
184
+ applyConfig(json.data);
136
185
  } catch (e) {
137
- error.value = `Ошибка загрузки: ${e.message}`;
186
+ // Сюда попадает только сбой сети: fetch не бросает на 4xx и 5xx
187
+ error.value = `Сервер недоступен: ${e.message}`;
138
188
  } finally {
139
189
  loading.value = false;
140
190
  }
141
191
  }
142
192
 
143
193
  async function saveConfig() {
194
+ // Пока идёт загрузка или сохранение, повторный запуск запрещён:
195
+ // иначе ответы двух запросов затирают друг друга
196
+ if (loading.value || saving.value) return;
197
+
144
198
  saving.value = true;
145
199
  message.value = "";
146
200
 
@@ -159,17 +213,28 @@ async function saveConfig() {
159
213
  headers: getHeaders(),
160
214
  body: JSON.stringify(payload),
161
215
  });
162
- const json = await res.json();
216
+ const json = await parseJsonBody(res);
163
217
 
164
- if (json.status === "ok") {
165
- message.value = json.message || "Конфиг сохранён";
166
- messageType.value = "ok";
167
- } else {
218
+ if (!res.ok) {
219
+ message.value = httpErrorText(res, json);
220
+ messageType.value = "error";
221
+ return;
222
+ }
223
+ if (!json) {
224
+ message.value = "Неожиданный ответ сервера: тело не JSON. Проверьте apiBase";
225
+ messageType.value = "error";
226
+ return;
227
+ }
228
+ if (json.status !== "ok") {
168
229
  message.value = json.message || "Ошибка сохранения";
169
230
  messageType.value = "error";
231
+ return;
170
232
  }
233
+
234
+ message.value = json.message || "Конфиг сохранён";
235
+ messageType.value = "ok";
171
236
  } catch (e) {
172
- message.value = `Ошибка сохранения: ${e.message}`;
237
+ message.value = `Сервер недоступен: ${e.message}`;
173
238
  messageType.value = "error";
174
239
  } finally {
175
240
  saving.value = false;
@@ -190,7 +255,7 @@ onMounted(() => {
190
255
  background: #fafafa;
191
256
  }
192
257
 
193
- .config-logs h2 {
258
+ .config-logs__title {
194
259
  margin: 0 0 16px 0;
195
260
  font-size: 18px;
196
261
  }
package/README.md CHANGED
@@ -12,7 +12,9 @@ npm install @andrey4emk/npm-app-front-b24
12
12
 
13
13
  ### Subscriptions
14
14
 
15
- Управление подписками на события и оффлайн-сущностями Bitrix24.
15
+ Управление подписками на события и оффлайн-очередью событий Bitrix24: список подписок,
16
+ подписка и отписка, просмотр и очистка оффлайн-сущностей, повторная запись сущности
17
+ с последующим снятием из очереди.
16
18
 
17
19
  ```vue
18
20
  <script setup>
@@ -24,11 +26,27 @@ import Subscriptions from "@andrey4emk/npm-app-front-b24/Subscriptions.vue";
24
26
  </template>
25
27
  ```
26
28
 
27
- Требует глобальный объект `$b24` (клиент Bitrix24 REST API).
29
+ **Props:**
30
+
31
+ | Prop | Тип | По умолчанию | Описание |
32
+ |------|-----|-------------|----------|
33
+ | `b24` | Object | `null` | Клиент Bitrix24 REST API (`B24Frame` из `@bitrix24/b24jssdk`) |
34
+ | `authConnector` | String | `""` | Значение `auth_connector` для методов работы с событиями |
35
+ | `portalDomain` | String | `""` | Адрес портала для ссылок на сущности, `portal.bitrix24.ru` или с `https://` |
36
+
37
+ Клиент ищется в таком порядке: prop `b24`, затем `inject("$b24")` (если приложение сделало
38
+ `app.provide("$b24", client)`), затем глобальный `window.$b24`. Без клиента компонент показывает
39
+ сообщение «Клиент Bitrix24 не инициализирован» и не падает.
40
+
41
+ Адрес портала без `portalDomain` берётся из `client.getTargetOrigin()`, а если его нет, из
42
+ `client.auth.getAuthData().domain`.
43
+
44
+ Состояние вкладок хранится внутри компонента, глобальный миксин с `tabs` и `selectTab` больше
45
+ не нужен.
28
46
 
29
47
  ### ConfigLogs
30
48
 
31
- Управление уровнями логирования через `config/log.json`.
49
+ Управление уровнями логирования через `config/log.json` на сервере.
32
50
 
33
51
  ```vue
34
52
  <script setup>
@@ -48,17 +66,66 @@ import ConfigLogs from "@andrey4emk/npm-app-front-b24/ConfigLogs.vue";
48
66
  | `secretKey` | String | `""` | Ключ авторизации (заголовок `secret`) |
49
67
  | `configName` | String | `"log"` | Имя конфига (без `.json`) |
50
68
 
69
+ Компонент ходит на `GET` и `POST` `${apiBase}/api/config/${configName}` и ждёт JSON вида
70
+ `{ status: "ok", data: {...} }`. Ошибки сети, ответы 401/403 и 5xx и не-JSON тела показываются
71
+ пользователю с кодом и текстом сервера.
72
+
73
+ ## Стили
74
+
75
+ Стили компонентов глобальные, без `scoped`, классы с префиксами `subscriptions__*` и
76
+ `config-logs__*`. Их можно переопределять из CSS приложения по имени класса.
77
+
78
+ В `Subscriptions` старые имена классов `tooltip`, `tooltipParent`, `switch`, `slider` остаются
79
+ на элементах вторым классом ради совместимости со стилями потребителей. Они будут сняты
80
+ в следующей minor-версии.
81
+
82
+ ## Типы
83
+
84
+ Пакет содержит `index.d.ts` с объявлениями props обоих компонентов. Чтобы TypeScript
85
+ потребителя (Vite + vue-tsc) их подхватил, подключите объявления явно, иначе сработает общий
86
+ shim для `*.vue` и props станут `any`:
87
+
88
+ ```ts
89
+ // env.d.ts
90
+ /// <reference types="@andrey4emk/npm-app-front-b24" />
91
+ ```
92
+
93
+ или в `tsconfig.json`:
94
+
95
+ ```json
96
+ {
97
+ "compilerOptions": {
98
+ "types": ["@andrey4emk/npm-app-front-b24"]
99
+ }
100
+ }
101
+ ```
102
+
51
103
  ## Требования
52
104
 
53
105
  - Vue 3 (`^3.0.0`) — peer dependency
54
106
  - Проект-потребитель должен поддерживать компиляцию `.vue` файлов (Vite / webpack + vue-loader)
55
- - `luxon` (`^3.7.2`) — используется в Subscriptions для форматирования дат
56
107
 
57
- ## Важно (v0.2.0)
108
+ Других зависимостей нет: даты форматируются через `Intl.DateTimeFormat`.
109
+
110
+ ## История версий
111
+
112
+ ### 0.3.0
113
+
114
+ - `Subscriptions`: состояние вкладок внутри компонента, props `b24`, `authConnector`,
115
+ `portalDomain`, адрес портала из клиента вместо захардкоженного домена, ссылки на
116
+ смарт-процессы
117
+ - `Subscriptions`: починены «Изменить» для смарт-процессов и «Удалить все», автообновление
118
+ останавливается при уходе со страницы, ошибки показываются на экране, подтверждение
119
+ удаления вторым кликом вместо `confirm()`
120
+ - `ConfigLogs`: разбор ответа сервера с проверкой `res.ok` и Content-Type
121
+ - Классы стилей с префиксами, добавлены стили тумблера
122
+ - Снята зависимость `luxon`
123
+ - Добавлен `index.d.ts` с типами props
58
124
 
59
- Начиная с версии `0.2.0` пакет поставляет **исходные `.vue` файлы** вместо скомпилированного бандла. Компиляция происходит на стороне проекта-потребителя.
125
+ ### 0.2.0
60
126
 
61
- Если вы обновляетесь с `0.1.x`, измените импорты:
127
+ Пакет поставляет исходные `.vue` файлы вместо скомпилированного бандла. Компиляция
128
+ происходит на стороне проекта-потребителя. При обновлении с `0.1.x` измените импорты:
62
129
 
63
130
  ```js
64
131
  // Было (0.1.x)
@@ -68,3 +135,20 @@ import "@andrey4emk/npm-app-front-b24/style.css";
68
135
  // Стало (0.2.0+)
69
136
  import Subscriptions from "@andrey4emk/npm-app-front-b24/Subscriptions.vue";
70
137
  ```
138
+
139
+ ## Публикация
140
+
141
+ Версия по semver: `patch` для исправлений, `minor` для новых функций, `major` для ломающих
142
+ изменений.
143
+
144
+ ```bash
145
+ git add .
146
+ git commit -m "upd(client): описание"
147
+ npm version minor # или patch / major
148
+ git push origin main --follow-tags
149
+ npm publish --access public
150
+ npm info @andrey4emk/npm-app-front-b24 version
151
+ ```
152
+
153
+ Если `npm publish` отвечает 403: проверить `npm whoami`, при необходимости `npm login`,
154
+ для scoped-пакета флаг `--access public` обязателен.