@smartex.com/modules-utils 0.1.0 → 0.1.3

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/README.md ADDED
@@ -0,0 +1,302 @@
1
+ # @smartex/modules-utils
2
+
3
+ Утилиты для создания, монтирования и управления жизненным циклом ES-модулей в рамках основного SPA-приложения.
4
+
5
+ Библиотека предоставляет два независимых набора инструментов:
6
+
7
+ - **base** — для виджет-модулей общего назначения (карточки, формы, информационные блоки).
8
+ - **survey** — для модулей опросов с полным управлением контекстом, историей навигации и ответами.
9
+
10
+ ## Установка
11
+
12
+ ```bash
13
+ npm install @smartex/modules-utils
14
+ ```
15
+
16
+ ### Peer-зависимости
17
+
18
+ Библиотека ожидает, что в проекте установлены:
19
+
20
+ ```bash
21
+ npm install react react-dom dayjs @smartex.com/ctx
22
+ ```
23
+
24
+ ## Виджет-модули (`base`)
25
+
26
+ ### Использование
27
+
28
+ Виджет-модуль — это React-компонент, который принимает `ModuleProps<E>`, где `E` — тип внешних данных, специфичных для конкретного модуля.
29
+
30
+ Определите компонент:
31
+
32
+ ```tsx
33
+ // participant-info.tsx
34
+ import type { ModuleProps } from '@smartex/modules-utils/base';
35
+
36
+ interface ExternalData {
37
+ participantId: string;
38
+ callHash: string;
39
+ }
40
+
41
+ export const ParticipantInfo = ({ token, externalData }: ModuleProps<ExternalData>) => {
42
+ return <div>{externalData?.participantId}</div>;
43
+ };
44
+ ```
45
+
46
+ Экспортируйте модуль через контроллер:
47
+
48
+ ```tsx
49
+ // index.tsx
50
+ import { createModuleController } from '@smartex/modules-utils/base';
51
+ import { ParticipantInfo } from './participant-info';
52
+
53
+ const moduleController = createModuleController(ParticipantInfo);
54
+
55
+ export default moduleController;
56
+ ```
57
+
58
+ Основное приложение монтирует модуль в DOM:
59
+
60
+ ```ts
61
+ const mod = await loadModule('participant-info');
62
+ mod.mount('target-element-id', {
63
+ token: '...',
64
+ apiConfig: { services: '...', externalApi: '...' },
65
+ externalData: { participantId: '123', callHash: 'abc' },
66
+ });
67
+
68
+ // При необходимости:
69
+ mod.unmount();
70
+ ```
71
+
72
+ ### API
73
+
74
+ #### `createModuleController<E>(Module)`
75
+
76
+ Создаёт контроллер с методами `mount` и `unmount`. Тип `E` (внешние данные) выводится автоматически из типа пропсов компонента.
77
+
78
+ #### `ModuleProps<T>`
79
+
80
+ | Поле | Тип | Обязательное | Описание |
81
+ | -------------- | ------------------------------------------- | ------------ | -------------------------------- |
82
+ | `token` | `string` | да | Токен авторизации |
83
+ | `lang` | `'ru' \| 'kz' \| 'en'` | нет | Язык интерфейса |
84
+ | `theme` | `string` | нет | Тема оформления |
85
+ | `externalData` | `T` | нет | Внешние данные для модуля |
86
+ | `apiConfig` | `{ services: string; externalApi: string }` | да | Конфигурация API |
87
+ | `onSuccess` | `() => void` | нет | Callback при успешном завершении |
88
+ | `onError` | `(errors: string[]) => void` | нет | Callback при ошибке |
89
+
90
+ ## Модули опросов (`survey`)
91
+
92
+ Модули опросов используют фабричный паттерн. Библиотека предоставляет headless-обёртку (логика без UI), а проект определяет компонент-layout (отображение).
93
+
94
+ ### Настройка в проекте
95
+
96
+ #### 1. Определите схему контекста
97
+
98
+ ```ts
99
+ // schema.ts
100
+ export const surveyCtxSchema = {
101
+ token: '',
102
+ user: { code: 0, name: '', role: { code: 0, name: '' }, companyCode: 0 },
103
+ session: { code: 0, id: '' },
104
+ lang: '',
105
+ theme: '',
106
+ };
107
+ ```
108
+
109
+ #### 2. Объявите глобальный тип `ctx`
110
+
111
+ ```ts
112
+ // ctx.d.ts
113
+ import type { SurveyCtxStore } from '@smartex/modules-utils/survey';
114
+ import type { surveyCtxSchema } from './schema';
115
+
116
+ declare global {
117
+ interface Window {
118
+ ctx: SurveyCtxStore<typeof surveyCtxSchema>;
119
+ }
120
+ var ctx: SurveyCtxStore<typeof surveyCtxSchema>;
121
+ }
122
+ ```
123
+
124
+ #### 3. Определите тип внешних данных
125
+
126
+ ```ts
127
+ // types.ts
128
+ export interface SurveyExternalData {
129
+ user: {
130
+ code: number;
131
+ name: string;
132
+ role: { code: number; name: string };
133
+ companyCode: number;
134
+ };
135
+ session: { code: number; id: string };
136
+ }
137
+ ```
138
+
139
+ #### 4. Создайте layout-компонент
140
+
141
+ Layout получает `mountProps` (данные вопроса, язык), `surveyProps` (обработчики: `onSetAnswer`, `goPrev` и т.д.) и `children` (сам модуль):
142
+
143
+ ```tsx
144
+ // survey-layout.tsx
145
+ import type { ReactNode } from 'react';
146
+ import type { SurveyMountProps, SurveyComponentProps } from '@smartex/modules-utils/survey';
147
+
148
+ interface SurveyLayoutProps {
149
+ mountProps: SurveyMountProps;
150
+ surveyProps: SurveyComponentProps;
151
+ children: ReactNode;
152
+ }
153
+
154
+ export const SurveyLayout = ({ mountProps, surveyProps, children }: SurveyLayoutProps) => {
155
+ const lang = mountProps.lang ?? 'ru';
156
+
157
+ return (
158
+ <div>
159
+ <h1>{mountProps.question.text[lang]}</h1>
160
+ <div>{children}</div>
161
+ {!mountProps.question.isPrevStepDisabled && (
162
+ <button onClick={surveyProps.goPrev}>Назад</button>
163
+ )}
164
+ </div>
165
+ );
166
+ };
167
+ ```
168
+
169
+ #### 5. Сконфигурируйте фабрику контроллера
170
+
171
+ Выполняется один раз. Все модули опросов импортируют результат:
172
+
173
+ ```ts
174
+ // survey-module-controller.ts
175
+ import { createSurveyModuleControllerFactory } from '@smartex/modules-utils/survey';
176
+ import { SurveyLayout } from './survey-layout';
177
+ import { surveyCtxSchema } from './schema';
178
+ import type { SurveyExternalData } from './types';
179
+
180
+ export const createSurveyModuleController =
181
+ createSurveyModuleControllerFactory<SurveyExternalData>(SurveyLayout, {
182
+ schema: surveyCtxSchema,
183
+ defaultLang: 'ru',
184
+ logicScreenTypes: ['DYNAMIC_ROUTING'],
185
+ });
186
+ ```
187
+
188
+ ### Создание модуля опроса
189
+
190
+ Модуль получает `SurveyComponentProps` — набор пропсов, подготовленных headless-обёрткой:
191
+
192
+ ```tsx
193
+ // list-selection.tsx
194
+ import type { SurveyComponentProps } from '@smartex/modules-utils/survey';
195
+
196
+ export const ListSelection = ({ question, onSetAnswer, goNext }: SurveyComponentProps) => {
197
+ const handleSelect = (answerCode: number, nextQuestion: number) => {
198
+ onSetAnswer(answerCode, null);
199
+ goNext(nextQuestion);
200
+ };
201
+
202
+ return (
203
+ <ul>
204
+ {question.answers.map((answer) => (
205
+ <li key={answer.code} onClick={() => handleSelect(answer.code, answer.nextQuestion!)}>
206
+ {answer.text.ru}
207
+ </li>
208
+ ))}
209
+ </ul>
210
+ );
211
+ };
212
+ ```
213
+
214
+ Экспорт через контроллер:
215
+
216
+ ```tsx
217
+ // index.tsx
218
+ import { createSurveyModuleController } from '@/modules/_lib/survey/survey-module-controller';
219
+ import { ListSelection } from './list-selection';
220
+
221
+ const moduleController = createSurveyModuleController(ListSelection);
222
+
223
+ export default moduleController;
224
+ ```
225
+
226
+ ### API
227
+
228
+ #### `createSurveyModuleControllerFactory<E>(Layout, config)`
229
+
230
+ Фабрика, возвращающая функцию `createSurveyModuleController`.
231
+
232
+ | Параметр | Тип | Описание |
233
+ | -------- | -------------------------- | ------------------------------------------------ |
234
+ | `Layout` | `SurveyLayoutComponent` | React-компонент для отображения обёртки модуля |
235
+ | `config` | `SurveyModuleWrapperConfig`| Конфигурация (см. ниже) |
236
+
237
+ #### `SurveyModuleWrapperConfig`
238
+
239
+ | Поле | Тип | Описание |
240
+ | ------------------ | ------------------------ | ------------------------------------------------------------ |
241
+ | `schema` | `Record<string, unknown>` | Схема контекста для `@smartex.com/ctx` |
242
+ | `defaultLang` | `'ru' \| 'kz' \| 'en'` | Язык по умолчанию |
243
+ | `logicScreenTypes` | `string[]` | Типы вопросов-экранов, пропускаемых при навигации назад |
244
+
245
+ #### `SurveyComponentProps`
246
+
247
+ Пропсы, которые получает каждый модуль опроса:
248
+
249
+ | Поле | Тип | Описание |
250
+ | ---------------- | ---------------------------------------------------- | ------------------------------------------- |
251
+ | `question` | `SurveyQuestion` | Данные текущего вопроса |
252
+ | `getSettings` | `<T>(key?: string) => [T \| null, string \| null]` | Получение настроек вопроса |
253
+ | `onSetAnswer` | `(answer: number \| number[], value: unknown) => void`| Сохранение ответа |
254
+ | `goNext` | `(questionCode: number) => void` | Переход к следующему вопросу |
255
+ | `goPrev` | `() => void` | Переход к предыдущему вопросу |
256
+ | `onSuccess` | `() => void` | Сигнал успешного завершения |
257
+ | `onError` | `(errors: string[]) => void` | Сигнал ошибки |
258
+ | `onSurveyFinish` | `() => void` | Завершение опроса |
259
+
260
+ #### `SurveyLayoutComponent`
261
+
262
+ Тип layout-компонента, ожидаемый фабрикой:
263
+
264
+ ```ts
265
+ type SurveyLayoutComponent = ComponentType<{
266
+ mountProps: SurveyMountProps;
267
+ surveyProps: SurveyComponentProps;
268
+ children: ReactNode;
269
+ }>;
270
+ ```
271
+
272
+ ## Инициализация контекста
273
+
274
+ Headless-обёртка автоматически управляет жизненным циклом `ctx`:
275
+
276
+ - Создаёт контекст на первом вопросе опроса.
277
+ - Записывает `token`, `lang`, `theme` из пропсов монтирования.
278
+ - Итерирует `externalData` и записывает каждый ключ через `ctx.set()`. Ключи `externalData` должны совпадать с ключами схемы — `ctx` валидирует их в рантайме.
279
+ - Уничтожает контекст при навигации назад с первого вопроса.
280
+
281
+ Проект не вызывает методы `ctx` напрямую при настройке — только модули используют глобальный `ctx` в своей логике.
282
+
283
+ ## Архитектура
284
+
285
+ ```
286
+ Основное приложение
287
+ └── module.mount(id, props) // знает только пропсы монтирования
288
+
289
+
290
+ Контроллер модуля // mount / unmount
291
+ └── createRoot + render
292
+
293
+ ├── SurveyModuleWrapper // библиотека: ctx, история, ответы (без UI)
294
+ │ └── children(injectedProps)
295
+ │ │
296
+ │ ▼
297
+ ├── SurveyLayout // проект: заголовок, кнопки, стили
298
+ │ └── children
299
+ │ │
300
+ │ ▼
301
+ └── Module // компонент модуля (SurveyComponentProps)
302
+ ```
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentType, ReactNode } from 'react';
3
3
 
4
- interface ModuleProps<T extends object = object> {
4
+ type ModuleProps<T extends object = object> = {
5
5
  token: string;
6
6
  lang?: 'ru' | 'kz' | 'en';
7
7
  theme?: string;
@@ -12,11 +12,11 @@ interface ModuleProps<T extends object = object> {
12
12
  };
13
13
  onSuccess?: () => void;
14
14
  onError?: (errors: string[]) => void;
15
- }
16
- interface ModuleController<T extends object = object> {
15
+ };
16
+ type ModuleController<T extends object = object> = {
17
17
  mount: (targetNodeId: string, props: ModuleProps<T>) => void;
18
18
  unmount: () => void;
19
- }
19
+ };
20
20
 
21
21
  declare const createModuleController: <E extends object = object>(Module: ComponentType<ModuleProps<E>>) => ModuleController<E>;
22
22
 
@@ -2,7 +2,7 @@ import { ComponentType, ReactNode } from 'react';
2
2
  import { SurveyResultEntry, CtxStoreWithSurvey } from '@smartex.com/ctx';
3
3
 
4
4
  type Lang = 'ru' | 'kz' | 'en';
5
- interface SurveyAnswer {
5
+ type SurveyAnswer = {
6
6
  code: number;
7
7
  text: {
8
8
  ru: string | null;
@@ -24,8 +24,8 @@ interface SurveyAnswer {
24
24
  isMandatory: number;
25
25
  isCustomRouting: boolean;
26
26
  isCustomInput: boolean;
27
- }
28
- interface SurveyQuestion {
27
+ };
28
+ type SurveyQuestion = {
29
29
  code: number;
30
30
  text: {
31
31
  ru: string | null;
@@ -48,8 +48,8 @@ interface SurveyQuestion {
48
48
  isFirst: boolean;
49
49
  isPrevStepDisabled: boolean;
50
50
  isModule: boolean;
51
- }
52
- interface SurveyMountProps<T extends object = object> {
51
+ };
52
+ type SurveyMountProps<T extends object = object> = {
53
53
  token: string;
54
54
  lang?: Lang;
55
55
  theme?: string;
@@ -66,7 +66,7 @@ interface SurveyMountProps<T extends object = object> {
66
66
  routeToQuestion: (questionCode: number) => void;
67
67
  onSurveyFinish: () => void;
68
68
  onError: (errors: string[]) => void;
69
- }
69
+ };
70
70
  type SurveyComponentProps = {
71
71
  question: SurveyMountProps['question'];
72
72
  getSettings: <T = unknown>(key?: string) => [T | null, string | null];
@@ -77,15 +77,15 @@ type SurveyComponentProps = {
77
77
  onError: (errors: string[]) => void;
78
78
  onSurveyFinish: SurveyMountProps['onSurveyFinish'];
79
79
  };
80
- interface SurveyModuleController<E extends object = object> {
80
+ type SurveyModuleController<E extends object = object> = {
81
81
  mount: (targetNodeId: string, props: SurveyMountProps<E>) => void;
82
82
  unmount: () => void;
83
- }
84
- interface SurveyModuleWrapperConfig {
83
+ };
84
+ type SurveyModuleWrapperConfig = {
85
85
  schema: Record<string, unknown>;
86
86
  defaultLang: Lang;
87
87
  logicScreenAliases: string[];
88
- }
88
+ };
89
89
  type SurveyCtxStore<S extends Record<string, unknown>> = CtxStoreWithSurvey<S>;
90
90
 
91
91
  type SurveyLayout = ComponentType<{
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "@smartex.com/modules-utils",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
- "files": ["dist"],
5
+ "files": [
6
+ "dist"
7
+ ],
6
8
  "exports": {
7
9
  "./base": {
8
10
  "types": "./dist/base/index.d.ts",
@@ -13,8 +15,19 @@
13
15
  "import": "./dist/survey/index.js"
14
16
  }
15
17
  },
18
+ "typesVersions": {
19
+ "*": {
20
+ "base": [
21
+ "dist/base/index.d.ts"
22
+ ],
23
+ "survey": [
24
+ "dist/survey/index.d.ts"
25
+ ]
26
+ }
27
+ },
16
28
  "scripts": {
17
- "build": "tsup",
29
+ "typecheck": "tsc --noEmit",
30
+ "build": "npm run typecheck && tsup",
18
31
  "prepublishOnly": "npm run build"
19
32
  },
20
33
  "peerDependencies": {