FSM-кнопки
Сущности MineFlow (актив, персонал, план производства, заявка на ТМЦ…) живут по конечному автомату состояний. Для каждого статуса разрешён лишь ограниченный набор переходов — и UI должен показывать ровно те кнопки действий, что разрешает машина в текущем статусе.
Источник истины для этих переходов — та же xstate-машина, что у бэкенда (ADR-0022 — entity = source of truth, xstate = spec). SDK re-export'ит её на фронт без копий, поэтому FSM-логика UI не дрейфует от серверной.
Этот рецепт: как из статуса сущности получить список действий и отрисовать кнопки.
Два экспорта
availableActions(machine, status)— чистая функция (@mineflow/client-react): по машине и строковому статусу возвращает массив имён переходов (string[]).useAvailableActions(machine, status)— React-хук-обёртка сuseMemoповерх той же функции.
Машины и status-энумы берутся из @mineflow/api-schemas — единственного browser/RN-safe пакета с FSM (доменные barrel'ы @mineflow/eam и т.п. тянут NestJS/Prisma и под фронт не собираются).
Базовый пример
import { assetMachine } from '@mineflow/api-schemas';
import { useAvailableActions } from '@mineflow/client-react';
const ACTION_LABELS: Record<string, string> = {
SEND_TO_MAINTENANCE: 'На ТО',
CONSERVE: 'Законсервировать',
REACTIVATE: 'Расконсервировать',
COMPLETE_MAINTENANCE: 'Завершить ТО',
DECOMMISSION: 'Списать',
};
function AssetActions({ asset }: { asset: { id: string; status: string } }) {
// имена переходов, разрешённых машиной в текущем статусе
const actions = useAvailableActions(assetMachine, asset.status);
// для 'operational' → ['SEND_TO_MAINTENANCE', 'CONSERVE', 'DECOMMISSION']
return (
<>
{actions.map((action) => (
<button key={action} onClick={() => runAction(asset, action)}>
{ACTION_LABELS[action] ?? action}
</button>
))}
</>
);
}
Без React то же самое чистой функцией — удобно для не-компонентного кода (вычисление доступности, тесты):
import { assetMachine } from '@mineflow/api-schemas';
import { availableActions } from '@mineflow/client-react';
availableActions(assetMachine, 'maintenance'); // ['COMPLETE_MAINTENANCE', 'DECOMMISSION']
availableActions(assetMachine, 'decommissioned'); // [] — терминальный статус, кнопок нет
Машина — параметр, а не вшита в хук. Берите подходящую из @mineflow/api-schemas: assetMachine, personnelMachine, productionPlanMachine, fuelSupplyRequestMachine, tmcRequestMachine и т.д. Статус — это строковый stateValue верхнего уровня машины (машины MineFlow плоские), он же значение поля status сущности из API. Полный список машин — в гайде @mineflow/api-schemas и в API-референсе.
Переходы возвращаются БЕЗ учёта guard'ов
availableActions отдаёт все имена переходов state-node, не вычисляя guard'ы (условия) xstate. Это намеренно:
- UI должен предложить действие, даже если оно требует доп. ввода. Например,
DECOMMISSIONу актива требует double-approval — но кнопку всё равно надо показать, по клику открыть форму, и только потом отправить запрос. - Финальную проверку делает бэкенд (см. ниже), guard'ы на фронте дублировали бы серверную логику и неизбежно бы с ней разошлись.
Наличие действия в списке availableActions означает «такой переход в принципе существует из этого статуса», а не «прямо сейчас точно пройдёт». Решение о фактической доступности (роль, заполненность полей, бизнес-инварианты) принимает сервер.
RBAC поверх FSM
availableActions отвечает только на вопрос «какие переходы разрешает машина статусов». Кто имеет право их выполнять — отдельная ось. Скрестите FSM с RBAC-гейтом useCan:
import { assetMachine } from '@mineflow/api-schemas';
import { useAvailableActions, useCan } from '@mineflow/client-react';
function AssetActions({ asset }: { asset: { id: string; status: string } }) {
const actions = useAvailableActions(assetMachine, asset.status);
const canManage = useCan(['Engineer', 'Mechanic']);
if (!canManage) return null;
return actions.map((action) => /* ... кнопки ... */);
}
Подробнее про роли и проверки прав — рецепт RBAC-гейтинг UI.