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:
@@ -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: 'Виды оборудования' }}
|
||||
|
||||
16
client/src/AppNotification.tsx
Normal file
16
client/src/AppNotification.tsx
Normal 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',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -1,18 +1,57 @@
|
||||
import { DataProvider, fetchUtils } from 'react-admin';
|
||||
import { DataProvider, fetchUtils, HttpError } from 'react-admin';
|
||||
import { getValidAccessToken } from './auth/keycloak';
|
||||
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' });
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
return fetchUtils.fetchJson(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
try {
|
||||
return await fetchUtils.fetchJson(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const fetchError = error as FetchJsonError;
|
||||
const fromPayload = userMessageFromApiBody(fetchError.body, '');
|
||||
const fallbackMessage = fetchError.message || 'Request failed';
|
||||
|
||||
throw new HttpError(
|
||||
fromPayload || fallbackMessage,
|
||||
fetchError.status ?? 500,
|
||||
fetchError.body,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function buildQueryString(query: Record<string, unknown>) {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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="Наименование единицы оборудования" />
|
||||
|
||||
@@ -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="Наименование единицы оборудования" />
|
||||
|
||||
@@ -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 })} />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user