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