@andrey4emk/npm-app-front-b24 0.1.20 → 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 ADDED
@@ -0,0 +1,402 @@
1
+ <template>
2
+ <div class="config-logs">
3
+ <h2 class="config-logs__title">Уровни логирования</h2>
4
+
5
+ <div v-if="loading" class="config-logs__loading">Загрузка...</div>
6
+
7
+ <div v-else-if="error" class="config-logs__error">
8
+ <p>{{ error }}</p>
9
+ <button @click="loadConfig">Повторить</button>
10
+ </div>
11
+
12
+ <div v-else>
13
+ <table class="config-logs__table">
14
+ <thead>
15
+ <tr>
16
+ <th>Уровень</th>
17
+ <th>Включён</th>
18
+ <th>JSON-формат</th>
19
+ </tr>
20
+ </thead>
21
+ <tbody>
22
+ <tr v-for="level in levels" :key="level.key">
23
+ <td>
24
+ <span class="config-logs__level-name">{{ level.label }}</span>
25
+ <span class="config-logs__level-key">{{ level.key }}</span>
26
+ </td>
27
+ <td>
28
+ <label class="config-logs__switch">
29
+ <input
30
+ type="checkbox"
31
+ v-model="config[level.key].enabled"
32
+ />
33
+ <span class="config-logs__slider"></span>
34
+ </label>
35
+ </td>
36
+ <td>
37
+ <label class="config-logs__switch">
38
+ <input
39
+ type="checkbox"
40
+ v-model="config[level.key].json"
41
+ />
42
+ <span class="config-logs__slider"></span>
43
+ </label>
44
+ </td>
45
+ </tr>
46
+ </tbody>
47
+ </table>
48
+
49
+ <div class="config-logs__actions">
50
+ <button @click="saveConfig" :disabled="loading || saving">
51
+ {{ saving ? "Сохранение..." : "Сохранить" }}
52
+ </button>
53
+ <button @click="loadConfig" :disabled="loading || saving">Обновить</button>
54
+ </div>
55
+
56
+ <div v-if="message" :class="['config-logs__message', messageType === 'ok' ? 'config-logs__message--success' : 'config-logs__message--error']">
57
+ {{ message }}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </template>
62
+
63
+ <script setup>
64
+ import { ref, reactive, onMounted } from "vue";
65
+
66
+ const props = defineProps({
67
+ apiBase: {
68
+ type: String,
69
+ default: "",
70
+ },
71
+ secretKey: {
72
+ type: String,
73
+ default: "",
74
+ },
75
+ configName: {
76
+ type: String,
77
+ default: "log",
78
+ },
79
+ });
80
+
81
+ const levels = [
82
+ { key: "trace", label: "Трассировка" },
83
+ { key: "debug", label: "Отладка" },
84
+ { key: "info", label: "Информация" },
85
+ { key: "warn", label: "Предупреждения" },
86
+ { key: "error", label: "Ошибки" },
87
+ ];
88
+
89
+ const config = reactive({
90
+ trace: { enabled: false, json: true, color: "" },
91
+ debug: { enabled: true, json: true, color: "" },
92
+ info: { enabled: true, json: true, color: "" },
93
+ warn: { enabled: true, json: true, color: "" },
94
+ error: { enabled: true, json: true, color: "" },
95
+ });
96
+
97
+ const loading = ref(false);
98
+ const saving = ref(false);
99
+ const error = ref("");
100
+ const message = ref("");
101
+ const messageType = ref("ok");
102
+
103
+ function getHeaders() {
104
+ const headers = { "Content-Type": "application/json" };
105
+ if (props.secretKey) {
106
+ headers.secret = props.secretKey;
107
+ }
108
+ return headers;
109
+ }
110
+
111
+ function getUrl() {
112
+ const base = props.apiBase.replace(/\/+$/, "");
113
+ return `${base}/api/config/${props.configName}`;
114
+ }
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
+
158
+ async function loadConfig() {
159
+ // Пока идёт загрузка или сохранение, повторный запуск запрещён:
160
+ // иначе ответы двух запросов затирают друг друга
161
+ if (loading.value || saving.value) return;
162
+
163
+ loading.value = true;
164
+ error.value = "";
165
+ message.value = "";
166
+
167
+ try {
168
+ const res = await fetch(getUrl(), { headers: getHeaders() });
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) {
180
+ error.value = json.message || "Не удалось загрузить конфиг";
181
+ return;
182
+ }
183
+
184
+ applyConfig(json.data);
185
+ } catch (e) {
186
+ // Сюда попадает только сбой сети: fetch не бросает на 4xx и 5xx
187
+ error.value = `Сервер недоступен: ${e.message}`;
188
+ } finally {
189
+ loading.value = false;
190
+ }
191
+ }
192
+
193
+ async function saveConfig() {
194
+ // Пока идёт загрузка или сохранение, повторный запуск запрещён:
195
+ // иначе ответы двух запросов затирают друг друга
196
+ if (loading.value || saving.value) return;
197
+
198
+ saving.value = true;
199
+ message.value = "";
200
+
201
+ const payload = {};
202
+ for (const level of levels) {
203
+ payload[level.key] = {
204
+ enabled: config[level.key].enabled,
205
+ color: config[level.key].color,
206
+ json: config[level.key].json,
207
+ };
208
+ }
209
+
210
+ try {
211
+ const res = await fetch(getUrl(), {
212
+ method: "POST",
213
+ headers: getHeaders(),
214
+ body: JSON.stringify(payload),
215
+ });
216
+ const json = await parseJsonBody(res);
217
+
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") {
229
+ message.value = json.message || "Ошибка сохранения";
230
+ messageType.value = "error";
231
+ return;
232
+ }
233
+
234
+ message.value = json.message || "Конфиг сохранён";
235
+ messageType.value = "ok";
236
+ } catch (e) {
237
+ message.value = `Сервер недоступен: ${e.message}`;
238
+ messageType.value = "error";
239
+ } finally {
240
+ saving.value = false;
241
+ }
242
+ }
243
+
244
+ onMounted(() => {
245
+ loadConfig();
246
+ });
247
+ </script>
248
+
249
+ <style>
250
+ .config-logs {
251
+ margin: 20px 0;
252
+ padding: 16px;
253
+ border: 1px solid #ddd;
254
+ border-radius: 8px;
255
+ background: #fafafa;
256
+ }
257
+
258
+ .config-logs__title {
259
+ margin: 0 0 16px 0;
260
+ font-size: 18px;
261
+ }
262
+
263
+ .config-logs__loading,
264
+ .config-logs__error {
265
+ padding: 12px;
266
+ text-align: center;
267
+ }
268
+
269
+ .config-logs__error {
270
+ color: #d32f2f;
271
+ }
272
+
273
+ .config-logs__table {
274
+ width: 100%;
275
+ border-collapse: collapse;
276
+ margin-bottom: 16px;
277
+ }
278
+
279
+ .config-logs__table th,
280
+ .config-logs__table td {
281
+ padding: 10px 12px;
282
+ text-align: left;
283
+ border-bottom: 1px solid #eee;
284
+ }
285
+
286
+ .config-logs__table th {
287
+ font-weight: 600;
288
+ color: #555;
289
+ font-size: 13px;
290
+ text-transform: uppercase;
291
+ }
292
+
293
+ .config-logs__level-name {
294
+ display: block;
295
+ font-weight: 500;
296
+ }
297
+
298
+ .config-logs__level-key {
299
+ display: block;
300
+ font-size: 12px;
301
+ color: #888;
302
+ font-family: monospace;
303
+ }
304
+
305
+ .config-logs__switch {
306
+ position: relative;
307
+ display: inline-block;
308
+ width: 44px;
309
+ height: 24px;
310
+ }
311
+
312
+ .config-logs__switch input {
313
+ opacity: 0;
314
+ width: 0;
315
+ height: 0;
316
+ }
317
+
318
+ .config-logs__slider {
319
+ position: absolute;
320
+ cursor: pointer;
321
+ top: 0;
322
+ left: 0;
323
+ right: 0;
324
+ bottom: 0;
325
+ background-color: #ccc;
326
+ transition: 0.3s;
327
+ border-radius: 24px;
328
+ }
329
+
330
+ .config-logs__slider::before {
331
+ content: "";
332
+ position: absolute;
333
+ height: 18px;
334
+ width: 18px;
335
+ left: 3px;
336
+ bottom: 3px;
337
+ background-color: #fff;
338
+ transition: 0.3s;
339
+ border-radius: 50%;
340
+ }
341
+
342
+ .config-logs__switch input:checked + .config-logs__slider {
343
+ background-color: #4caf50;
344
+ }
345
+
346
+ .config-logs__switch input:checked + .config-logs__slider::before {
347
+ transform: translateX(20px);
348
+ }
349
+
350
+ .config-logs__actions {
351
+ display: flex;
352
+ gap: 8px;
353
+ }
354
+
355
+ .config-logs__actions button {
356
+ padding: 8px 20px;
357
+ border: 1px solid #ccc;
358
+ border-radius: 4px;
359
+ background: #fff;
360
+ cursor: pointer;
361
+ font-size: 14px;
362
+ transition: background 0.2s;
363
+ }
364
+
365
+ .config-logs__actions button:hover:not(:disabled) {
366
+ background: #f0f0f0;
367
+ }
368
+
369
+ .config-logs__actions button:first-child {
370
+ background: #4caf50;
371
+ color: #fff;
372
+ border-color: #4caf50;
373
+ }
374
+
375
+ .config-logs__actions button:first-child:hover:not(:disabled) {
376
+ background: #43a047;
377
+ }
378
+
379
+ .config-logs__actions button:disabled {
380
+ opacity: 0.6;
381
+ cursor: not-allowed;
382
+ }
383
+
384
+ .config-logs__message {
385
+ margin-top: 12px;
386
+ padding: 8px 12px;
387
+ border-radius: 4px;
388
+ font-size: 14px;
389
+ }
390
+
391
+ .config-logs__message--success {
392
+ background: #e8f5e9;
393
+ color: #2e7d32;
394
+ border: 1px solid #a5d6a7;
395
+ }
396
+
397
+ .config-logs__message--error {
398
+ background: #ffebee;
399
+ color: #c62828;
400
+ border: 1px solid #ef9a9a;
401
+ }
402
+ </style>
package/README.md CHANGED
@@ -1,27 +1,154 @@
1
- Установка:
1
+ # @andrey4emk/npm-app-front-b24
2
+
3
+ Vue 3 компоненты для Bitrix24.
4
+
5
+ ## Установка
2
6
 
3
7
  ```bash
4
8
  npm install @andrey4emk/npm-app-front-b24
5
9
  ```
6
10
 
7
- Внимание по стилям: пакет содержит отдельный CSS-файл `dist/style.css`. Есть три варианта подключить стили в проекте-потребителе:
11
+ ## Компоненты
8
12
 
9
- 1. Явно импортировать CSS вместе с компонентом:
13
+ ### Subscriptions
10
14
 
11
- ```js
12
- import "@andrey4emk/npm-app-front-b24/dist/style.css";
13
- import { Subscriptions } from "@andrey4emk/npm-app-front-b24";
15
+ Управление подписками на события и оффлайн-очередью событий Bitrix24: список подписок,
16
+ подписка и отписка, просмотр и очистка оффлайн-сущностей, повторная запись сущности
17
+ с последующим снятием из очереди.
18
+
19
+ ```vue
20
+ <script setup>
21
+ import Subscriptions from "@andrey4emk/npm-app-front-b24/Subscriptions.vue";
22
+ </script>
23
+
24
+ <template>
25
+ <Subscriptions />
26
+ </template>
27
+ ```
28
+
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
+ не нужен.
46
+
47
+ ### ConfigLogs
48
+
49
+ Управление уровнями логирования через `config/log.json` на сервере.
50
+
51
+ ```vue
52
+ <script setup>
53
+ import ConfigLogs from "@andrey4emk/npm-app-front-b24/ConfigLogs.vue";
54
+ </script>
55
+
56
+ <template>
57
+ <ConfigLogs api-base="" :secret-key="secretKey" />
58
+ </template>
14
59
  ```
15
60
 
16
- 2. Использовать удобный короткий импорт (поддерживается):
61
+ **Props:**
62
+
63
+ | Prop | Тип | По умолчанию | Описание |
64
+ |------|-----|-------------|----------|
65
+ | `apiBase` | String | `""` | Базовый URL сервера |
66
+ | `secretKey` | String | `""` | Ключ авторизации (заголовок `secret`) |
67
+ | `configName` | String | `"log"` | Имя конфига (без `.json`) |
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
+
103
+ ## Требования
104
+
105
+ - Vue 3 (`^3.0.0`) — peer dependency
106
+ - Проект-потребитель должен поддерживать компиляцию `.vue` файлов (Vite / webpack + vue-loader)
107
+
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
124
+
125
+ ### 0.2.0
126
+
127
+ Пакет поставляет исходные `.vue` файлы вместо скомпилированного бандла. Компиляция
128
+ происходит на стороне проекта-потребителя. При обновлении с `0.1.x` измените импорты:
17
129
 
18
130
  ```js
19
- import "@andrey4emk/npm-app-front-b24/style.css";
131
+ // Было (0.1.x)
20
132
  import { Subscriptions } from "@andrey4emk/npm-app-front-b24";
133
+ import "@andrey4emk/npm-app-front-b24/style.css";
134
+
135
+ // Стало (0.2.0+)
136
+ import Subscriptions from "@andrey4emk/npm-app-front-b24/Subscriptions.vue";
21
137
  ```
22
138
 
23
- Рекомендация: импортируйте CSS явно. Поскольку библиотека выделяет стили в отдельный файл `dist/style.css`, автоматическое применение стилей при простом импорте JS не гарантируется во всех окружениях. Самые надёжные варианты:
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
+ ```
24
152
 
25
- - `import '@andrey4emk/npm-app-front-b24/style.css'` короткий импорт (рекомендую);
26
- - `import '@andrey4emk/npm-app-front-b24/dist/style.css'` явный путь к файлу;
27
- - подключение через `<link>` в `index.html`.
153
+ Если `npm publish` отвечает 403: проверить `npm whoami`, при необходимости `npm login`,
154
+ для scoped-пакета флаг `--access public` обязателен.