feat: align RU validation, error contract, and generator runtime templates

Wire DSL-derived field labels, safe API error JSON (string|string[]), decimal/enum DTO fixes, and client dataProvider without comma-splitting. Add generation/templates/runtime as canonical source copied on generate; extend AID bundle, prompts, validation gate, and docs.
This commit is contained in:
time_
2026-03-29 10:39:54 +03:00
parent 9a1a700efa
commit f6cdeec918
25 changed files with 801 additions and 93 deletions

View File

@@ -1,6 +1,7 @@
import { Admin, Resource } from 'react-admin';
import dataProvider from './dataProvider';
import authProvider from './auth/authProvider';
import { AppNotification } from './AppNotification';
import { EquipmentTypeList } from './resources/equipment-type/EquipmentTypeList';
import { EquipmentTypeCreate } from './resources/equipment-type/EquipmentTypeCreate';
@@ -18,7 +19,12 @@ import { RepairOrderEdit } from './resources/repair-order/RepairOrderEdit';
import { RepairOrderShow } from './resources/repair-order/RepairOrderShow';
const App = () => (
<Admin dataProvider={dataProvider} authProvider={authProvider} requireAuth>
<Admin
dataProvider={dataProvider}
authProvider={authProvider}
notification={AppNotification}
requireAuth
>
<Resource
name="equipment-types"
options={{ label: 'Виды оборудования' }}

View File

@@ -0,0 +1,16 @@
import { Notification, NotificationProps } from 'react-admin';
export const AppNotification = (props: NotificationProps) => (
<Notification
{...props}
sx={{
whiteSpace: 'pre-line',
'& .MuiAlert-message': {
whiteSpace: 'pre-line',
},
'& .MuiSnackbarContent-message': {
whiteSpace: 'pre-line',
},
}}
/>
);

View File

@@ -4,6 +4,33 @@ import { env } from './config/env';
const apiUrl = env.apiUrl;
/** HTTP status from fetch / react-admin error objects (avoid coupling the client to Nest). */
type HttpStatusCode = number;
/** JSON body shape returned by ApiExceptionFilter (and compatible Nest errors). */
type ApiErrorBody = {
message?: string | string[];
code?: string;
details?: unknown;
};
/** Shape thrown by fetchUtils.fetchJson on non-2xx responses. */
type FetchJsonError = {
status?: HttpStatusCode;
body?: ApiErrorBody;
message?: string;
};
function userMessageFromApiBody(
body: ApiErrorBody | undefined,
fallback: string,
): string {
const raw = body?.message;
if (Array.isArray(raw)) return raw.join('\n');
if (typeof raw === 'string') return raw;
return fallback;
}
const httpClient = async (url: string, options: fetchUtils.Options = {}) => {
const token = await getValidAccessToken();
const headers = new Headers(options.headers ?? { Accept: 'application/json' });
@@ -15,23 +42,14 @@ const httpClient = async (url: string, options: fetchUtils.Options = {}) => {
headers,
});
} catch (error: unknown) {
const e = error as {
status?: number;
body?: {
message?: string | string[];
details?: unknown;
};
message?: string;
};
const messageFromBody = e?.body?.message;
const normalizedMessage = Array.isArray(messageFromBody)
? messageFromBody.join(', ')
: messageFromBody;
const fetchError = error as FetchJsonError;
const fromPayload = userMessageFromApiBody(fetchError.body, '');
const fallbackMessage = fetchError.message || 'Request failed';
throw new HttpError(
normalizedMessage || e?.message || 'Request failed',
e?.status ?? 500,
e?.body,
fromPayload || fallbackMessage,
fetchError.status ?? 500,
fetchError.body,
);
}
};

View File

@@ -10,7 +10,7 @@ const statusChoices = [
export const EquipmentEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="id" label="id" disabled />
<TextInput source="id" label="Идентификатор" disabled />
<TextInput source="inventoryNumber" label="Инвентарный номер" isRequired />
<TextInput source="serialNumber" label="Заводской (серийный) номер" />
<TextInput source="name" label="Наименование единицы оборудования" isRequired />

View File

@@ -47,7 +47,7 @@ const EquipmentListActions = () => (
export const EquipmentList = () => (
<List actions={<EquipmentListActions />} filters={equipmentFilters} sort={{ field: 'inventoryNumber', order: 'ASC' }}>
<Datagrid rowClick="show">
<TextField source="id" label="id" />
<TextField source="id" label="Идентификатор" />
<TextField source="inventoryNumber" label="Инвентарный номер" />
<TextField source="serialNumber" label="Заводской (серийный) номер" />
<TextField source="name" label="Наименование единицы оборудования" />

View File

@@ -9,7 +9,7 @@ const statusChoices = [
export const EquipmentShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="id" label="id" />
<TextField source="id" label="Идентификатор" />
<TextField source="inventoryNumber" label="Инвентарный номер" />
<TextField source="serialNumber" label="Заводской (серийный) номер" />
<TextField source="name" label="Наименование единицы оборудования" />

View File

@@ -20,7 +20,7 @@ const statusChoices = [
export const RepairOrderEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="id" label="id" disabled />
<TextInput source="id" label="Идентификатор" disabled />
<TextInput source="number" label="Номер заявки" isRequired />
<ReferenceInput source="equipmentId" reference="equipment">
<AutocompleteInput label="Оборудование" optionText={(record) => record.inventoryNumber ? `${record.inventoryNumber}${record.name ?? record.inventoryNumber}` : (record.name ?? record.id)} filterToQuery={(searchText) => ({ q: searchText })} />

View File

@@ -58,7 +58,7 @@ const RepairOrderListActions = () => (
export const RepairOrderList = () => (
<List actions={<RepairOrderListActions />} filters={repairOrderFilters} sort={{ field: 'number', order: 'ASC' }}>
<Datagrid rowClick="show">
<TextField source="id" label="id" />
<TextField source="id" label="Идентификатор" />
<TextField source="number" label="Номер заявки" />
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
<TextField source="inventoryNumber" />

View File

@@ -19,7 +19,7 @@ const statusChoices = [
export const RepairOrderShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="id" label="id" />
<TextField source="id" label="Идентификатор" />
<TextField source="number" label="Номер заявки" />
<ReferenceField source="equipmentId" reference="equipment" label="Оборудование" link="show">
<TextField source="inventoryNumber" />