docs: add porting guide and shared Dataverse helpers
Common rules for moving the remaining stroy-bot-* sections into the monorepo, so every section follows the same structure and keeps 1:1 behaviour with its old web part. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# Как переносится раздел из stroy-bot-* в монорепо
|
||||
|
||||
Образец — главная страница: `src/webparts/stroyBot/pages/hub/` + `app/project/`.
|
||||
|
||||
## Главное правило
|
||||
**Поведение 1:1 со старым web part.** Те же запросы (URL, `$select`, `$filter`, `$expand`, `$top`, `$orderby`), те же поля, те же правила доступа, **те же тексты** (дословно), те же валидации. Известные баги переносятся как есть. Любое сознательное отличие записывается в `docs/MIGRATION-CHECKLIST.md` в блок «Сознательные отличия».
|
||||
|
||||
Меняется только слой интерфейса: HTML-строки и `innerHTML` → React-компоненты на `@tnsu/ui-kit`.
|
||||
|
||||
## Структура раздела
|
||||
```
|
||||
pages/<раздел>/
|
||||
<Раздел>Page.tsx — страница: собирает компоненты, обрабатывает loading / error / empty
|
||||
<Раздел>Page.module.scss
|
||||
texts.ts — ВСЕ пользовательские тексты раздела (дословно из старого кода)
|
||||
<раздел>Types.ts — типы данных
|
||||
<раздел>Repository.ts — запросы: функции-конструкторы URL + функции загрузки/записи
|
||||
<раздел>Repository.test.ts — тесты URL и сценариев на fakeHttpClient
|
||||
<раздел>Rules.ts — чистые функции (права, валидация, дерево, форматирование)
|
||||
<раздел>Rules.test.ts
|
||||
use<Что-то>.ts — хуки состояния (загрузка, сохранение)
|
||||
<Компонент>.tsx — под-компоненты, один компонент — один файл
|
||||
```
|
||||
|
||||
## Правила кода (стайл-гайд проекта)
|
||||
- TypeScript строго, **без `any`**. Ответы сервера описываются интерфейсами.
|
||||
- Компонент ≤ ~300 строк, функция ≤ ~50 строк. Больше — разбивать.
|
||||
- Логика — в `*Rules.ts` / `*Repository.ts` / хуках, не в JSX.
|
||||
- Имена списков — `LISTS` из `config/lists.ts`; таблицы Dataverse — `DV_TABLES` из `config/dataverse.ts`; адреса — из `useAppServices().settings`.
|
||||
- Внутренние имена полей SharePoint / колонок Dataverse — константами в `*Repository.ts` раздела (схемы в разных старых репо отличаются — не унифицировать без проверки).
|
||||
- Комментарии — «почему», а не «что». Без мёртвого кода.
|
||||
- `null` в своих типах не использовать (линтер) — `undefined`. Внешние ответы с `null` приводить через `?? undefined`.
|
||||
- Стили: SCSS-модули, `@import '../../../../styles/design-tokens';`, токены вместо цветов. Без magic numbers без комментария.
|
||||
- Форматирование — `.prettierrc` (одинарные кавычки, 120 символов, без висячих запятых).
|
||||
|
||||
## Данные
|
||||
- Клиенты: `const { sharePoint, dataverse, tnsuApi, settings, page } = useAppServices();`
|
||||
- SharePoint: `shared/sharepoint/spRest.ts` (`listItemsUrl`, `getJson`, `fetchCurrentUserId`). Запись — `sharePoint.fetch(url, { method: 'POST', headers, body })` с **теми же заголовками**, что в старом коде (verbose `__metadata` / `X-HTTP-Method: MERGE` / `IF-MATCH` — как было).
|
||||
- Dataverse: `shared/dataverse/dataverseRest.ts` (`dataverseUrl`, `dataverseGet/Post/Patch/Delete`).
|
||||
- Корпоративный API: `tnsuApi.fetch(`${settings.apiBaseUrl}/...`)`.
|
||||
- Текущий пользователь: `page.userId` (legacyPageContext.userId) или `fetchCurrentUserId` — как в старом коде раздела.
|
||||
- Маршрут: `useRouter()` → `route.projectId`, `route.objectId`, `route.reportId`, `route.copyFromId`; переход — `navigate({ page, projectId, ... })`; ссылка — `<RouteLink to={...}>`.
|
||||
- Ссылки старых web parts на другие страницы (`SendReport.aspx?...`, `worksPageUrl`) → `navigate` / `RouteLink` на раздел приложения.
|
||||
|
||||
## UI
|
||||
- ui-kit: `Button`, `IconButton`, `Dialog`, `confirmModal`, `Alert`, `Badge`, `Spinner`, `TextInput`, `TextArea`, `Select`, `Checkbox`, `Tabs`, `DataTable`, `FormCard`, `FormField`, `FormGrid`, `FormActions`, `FormHeader`, `Icon` (имена — `ICON_NAMES` в ките).
|
||||
- `alert()` / `confirm()` → `Alert` / `confirmModal` (тексты те же).
|
||||
- Чего нет в ките (дерево, тосты, контекстное меню, хлебные крошки) — делать в `pages/<раздел>/` своего раздела. Существующие файлы в `shared/` не менять; `EmptyState` и `useOutsideClose` — переиспользовать.
|
||||
- Всегда явные состояния: загрузка (`Spinner`), ошибка (`Alert variant="err"`), пусто (`EmptyState`).
|
||||
|
||||
## Тесты
|
||||
- `shared/testing/fakeHttpClient.ts` — заглушка: проверяем точные URL и тела запросов против старого кода, сценарии успеха и ошибок.
|
||||
- Чистые функции правил — отдельными тестами.
|
||||
- Запуск: `npx heft test --clean`. Должно быть 0 ошибок линтера и все тесты зелёные.
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ensureOk, IHttpClient, IHttpRequest } from '../../services/http/IHttpClient';
|
||||
import { DATAVERSE_API_PATH } from '../../config/dataverse';
|
||||
|
||||
// Общие помощники для Dataverse Web API. Заголовки — те же, что в старых web parts
|
||||
// (stroy-bot-works / stroy-bot-report): OData 4.0, JSON.
|
||||
|
||||
export const DATAVERSE_HEADERS: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'OData-Version': '4.0',
|
||||
'OData-MaxVersion': '4.0'
|
||||
};
|
||||
|
||||
/** `{dataverseUrl}/api/data/v9.2/{path}` */
|
||||
export function dataverseUrl(baseUrl: string, path: string): string {
|
||||
return `${baseUrl}${DATAVERSE_API_PATH}${path}`;
|
||||
}
|
||||
|
||||
/** Ответ Dataverse на запрос коллекции. */
|
||||
export interface IDataverseCollection<T> {
|
||||
value: T[];
|
||||
'@odata.nextLink'?: string;
|
||||
}
|
||||
|
||||
async function send(client: IHttpClient, url: string, request: IHttpRequest): Promise<unknown> {
|
||||
const response = await ensureOk(
|
||||
await client.fetch(url, { ...request, headers: { ...DATAVERSE_HEADERS, ...request.headers } })
|
||||
);
|
||||
// 204 No Content (PATCH/DELETE без Prefer: return=representation) — тела нет.
|
||||
if (response.status === 204) {
|
||||
return undefined;
|
||||
}
|
||||
const text = await response.text();
|
||||
return text ? JSON.parse(text) : undefined;
|
||||
}
|
||||
|
||||
export async function dataverseGet<T>(client: IHttpClient, url: string): Promise<T> {
|
||||
return (await send(client, url, { method: 'GET' })) as T;
|
||||
}
|
||||
|
||||
/** POST. С `Prefer: return=representation` Dataverse возвращает созданную запись. */
|
||||
export async function dataversePost<T>(
|
||||
client: IHttpClient,
|
||||
url: string,
|
||||
body: unknown,
|
||||
headers: Record<string, string> = {}
|
||||
): Promise<T | undefined> {
|
||||
return (await send(client, url, { method: 'POST', body: JSON.stringify(body), headers })) as T | undefined;
|
||||
}
|
||||
|
||||
export async function dataversePatch(
|
||||
client: IHttpClient,
|
||||
url: string,
|
||||
body: unknown,
|
||||
headers: Record<string, string> = {}
|
||||
): Promise<void> {
|
||||
await send(client, url, { method: 'PATCH', body: JSON.stringify(body), headers });
|
||||
}
|
||||
|
||||
export async function dataverseDelete(client: IHttpClient, url: string): Promise<void> {
|
||||
await send(client, url, { method: 'DELETE' });
|
||||
}
|
||||
Reference in New Issue
Block a user