qp-workflow-react
v1.0.4
Published
React components and hooks for visual workflow design and execution. Pairs with Chd.Workflow .NET backend.
Maintainers
Readme
qp-workflow-react 🎨
Chd (Cleverly Handle Difficulty) library helps you cleverly handle difficulty, write code quickly, and keep your application stable.
qp-workflow-react is the React + TypeScript companion to the Chd.Workflow .NET engine. It gives you everything you need to design workflows visually and run them in production with auto-generated forms, validation, guards, and transition handling — without writing UI plumbing yourself.
The library is small, framework-agnostic at the data layer (WorkflowClient), and works with any backend that follows the same REST contract.
📑 Table of Contents
- Why qp-workflow-react?
- Features
- Installation
- Quick Start
- Components
- Hook:
useWorkflow - Client:
WorkflowClient - TypeScript types
- Field types & validation
- SQL-backed dropdown options
- External forms
- Localization
- Backend contract
- Build & publish
- Troubleshooting
- Related packages
- License
💡 Why qp-workflow-react?
A typical workflow UI is more than a form — it has to talk to the backend, render fields based on the active node, validate before transitioning, ask for comments / confirmations, show available actions per role, resolve dropdowns from server data, and gracefully handle guard failures.
Without a library you end up writing the same plumbing in every project:
// Manual approach: lots of state, fetch, validation per component
const [state, setState] = useState(null);
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
useEffect(() => { fetch(`/api/workflow/instances/${id}/state`).then(r => r.json()).then(setState); }, [id]);
function submit(action) {
// …validate manually, build body, fetch, refresh state, handle guard 400, …
}
return state?.fields.map(f => /* render the right input for each FieldType */);With qp-workflow-react the same screen becomes a single component:
import { WorkflowRunner } from 'qp-workflow-react';
<WorkflowRunner
apiUrl="http://localhost:5035/api/workflow"
instanceId={instanceId}
locale="en"
theme="dark"
onCompleted={(i) => navigate(`/done/${i.id}`)}
/>The runner takes care of:
- fetching
/instances/{id}/stateon mount and after every transition, - rendering all 18 field types,
- client-side validation (required, min/max, pattern, typed checks for number/date/email/phone/currency),
requiresConfirmationandrequiresCommentmodals,- resolving SQL-backed
optionsQuerydropdowns at runtime, - displaying guard rejections (
GUARD_FAILED) inline.
The same is true for the designer — TreeDesigner ships a complete visual editor with action / guard / role pickers populated from the backend, so non-developers can update the flow without touching code.
| Aspect | Hand-written UI | qp-workflow-react |
|---|---|---|
| New form fields | New JSX + new validation | Add a Field to the node |
| New approval step | New screen + new fetch logic | Drag a node in TreeDesigner |
| Switching language | Custom translation glue | locale="tr" |
| Guard / confirm modals | Bespoke modals | Built into WorkflowRunner |
| External legacy form | Custom redirect + callback | formType: "External" + helper hooks |
✨ Features
| Feature | Description |
|---|---|
| 🌳 TreeDesigner | Visual, tree-based workflow editor with node, transition, field, guard, and rule editors |
| 🏃 WorkflowRunner | Drop-in runtime UI with auto-generated forms, validation, and transition handling |
| ⚛️ useWorkflow | Same runtime behaviour, but you keep full control of the rendering |
| 📡 WorkflowClient | Typed, framework-agnostic REST client for the workflow API |
| 🔗 External form helpers | Link a non-React page into the workflow via a single URL |
| 🛠️ Notification admin modal | Edit SMTP + group-resolution settings from the UI |
| 🌍 Localization | English and Turkish out of the box (LocaleProvider + useLocale) |
| 🎨 Themes | Dark and light themes built-in |
| 🛡 Guard-aware | Automatic display of GUARD_FAILED rejections + warning / confirm flows |
| 🔒 Safe options | Reads SQL-backed options from /field-options/{name} — never touches raw SQL |
| 📦 ESM + CJS + .d.ts | Ships full TypeScript typings; React 18+ peer dependency |
📦 Installation
npm install qp-workflow-reactPeer dependency: React 18+. A running Chd.Workflow (or any backend that follows the same REST contract) is required.
🚀 Quick Start
import {
TreeDesigner,
WorkflowRunner,
WorkflowClient,
} from 'qp-workflow-react';
const apiUrl = 'http://localhost:5035/api/workflow';
// 1. Design the workflow visually
<TreeDesigner
apiUrl={apiUrl}
initialDefinitionId="leave-request"
locale="en"
enableFormEditor
onSaved={(def) => console.log('saved', def.id)}
/>
// 2. Create an instance and run it
const client = new WorkflowClient({ apiUrl });
const instance = await client.createInstance('leave-request', { employeeId: '123' });
<WorkflowRunner
apiUrl={apiUrl}
instanceId={instance.id}
locale="en"
theme="dark"
onCompleted={(i) => console.log('done', i.id)}
/>🧩 Components
TreeDesigner
The main, tree-based designer. It loads / saves through the workflow REST API and pulls action, guard, and participant-group metadata from the backend.
<TreeDesigner
apiUrl="http://localhost:5035/api/workflow"
initialDefinitionId="leave-request"
locale="tr"
enableFormEditor
onSaved={(def) => console.log(def.id)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
| apiUrl | string | — | Workflow API base URL. |
| initialDefinitionId? | string | 'workflow-1' | Definition to load on mount; if it doesn't exist a default tree is generated. |
| locale? | 'en' \| 'tr' | 'en' | UI language. |
| enableFormEditor? | boolean | false | Show the FieldEditor for designing dynamic forms per node. |
| onSaved? | (def: WorkflowDefinition) => void | — | Fired after a successful save. |
Highlights:
- Right-click a node to add a child or insert a transition; double-click to rename.
- Action / guard dropdowns are populated from
GET /actionsandGET /guards, so authoring always reflects the running backend. - The transition editor includes a multi-clause guard builder (AND / OR rows with field / operator / value pickers) on top of
Transition.Guard. - The "Allowed roles" picker is fed from
GET /admin/participant-groups. FieldEditor(whenenableFormEditoris on) supports all 18 field types and editsoptionsQuery,defaultValue, validation, etc.
WorkflowRunner
A complete runtime UI. Give it an instance id and it renders the current node's form, validates input, surfaces guard / confirmation / comment flows, and calls the transition API.
<WorkflowRunner
apiUrl="http://localhost:5035/api/workflow"
instanceId="instance-abc123"
locale="en"
theme="dark"
onTransition={(action) => console.log('moved by', action)}
onCompleted={(instance) => console.log('done', instance.id)}
onError={(err) => console.error(err)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
| apiUrl | string | — | Workflow API base URL. |
| instanceId | string | — | Instance to render. |
| theme? | 'dark' \| 'light' | 'dark' | Built-in palette. |
| locale? | 'en' \| 'tr' | 'en' | UI language. |
| debug? | boolean | false | Show a small debug strip (current node, raw state). |
| onCompleted? | (instance) => void | — | Fired when the instance enters an End node. |
| onTransition? | (action, newState) => void | — | Fired after every successful transition. |
| onError? | (error) => void | — | Fired on API / validation errors. |
| loadingComponent? | ReactNode | — | Replace the built-in spinner. |
| errorComponent? | (error, retry) => ReactNode | — | Replace the built-in error screen. |
| completedComponent? | (node, instance) => ReactNode | — | Replace the built-in completion screen. |
| className?, style? | — | — | Pass-through styling. |
Built-in behaviour:
- Calls
GET /instances/{id}/stateon mount and after every transition. - Renders all 18 field types automatically; required + min/max + regex + typed validation (number, decimal, date, datetime, time, email, phone, currency) is applied client-side before the transition fires.
- Honours
transition.requiresConfirmation(modal with title / body) andtransition.requiresComment(comment textarea modal). - Detects
node.formType === 'External'and usesbuildExternalFormUrlto redirect with workflow context query params. - For
Dropdown/Radio/MultiSelectfields whose definition contains anoptionsQuery, options are fetched viaGET /instances/{id}/field-options/{fieldName}so raw SQL is never sent to the browser. - Surfaces guard rejections (
{ code: "GUARD_FAILED" }) as inline errors with the message from the backend.
WorkflowDesigner (form-based)
A simpler, form-driven editor (no canvas) kept for hosts that prefer a non-visual UX. Same general props as TreeDesigner.
import { WorkflowDesigner } from 'qp-workflow-react';
<WorkflowDesigner
apiUrl="http://localhost:5035/api/workflow"
initialDefinitionId="leave-request"
onSaved={(def) => console.log(def.id)}
/>WorkflowHostNotificationSettingsModal
Admin modal for the host application that exposes the SMTP and group-resolution settings stored on the backend AppSettings table. It uses WorkflowClient.getNotificationAdminSettings() / updateNotificationAdminSettings() under the hood.
import { WorkflowHostNotificationSettingsModal } from 'qp-workflow-react';
<WorkflowHostNotificationSettingsModal
open={open}
onClose={() => setOpen(false)}
apiUrl="http://localhost:5035/api/workflow"
locale="en"
/>The component masks smtpPassword (the GET response only carries smtpPasswordSet: boolean) and treats an empty password on PUT as "keep the stored value".
⚛️ Hook: useWorkflow
If WorkflowRunner is too opinionated for your design, useWorkflow lets you keep full UI control while reusing the same runtime logic (state loading, option resolution, validation helpers, transition handling).
import { useWorkflowReact, WorkflowClient } from 'qp-workflow-react';
const client = new WorkflowClient({ apiUrl });
function MyRunner({ instanceId }: { instanceId: string }) {
const {
state, instance, currentNode, fields, availableActions, history, breadcrumb,
values, errors, isLoading, error,
setValue, setValues, transition, validate, reload, createInstance, cancel,
} = useWorkflowReact({ client, instanceId });
if (isLoading) return <p>Loading…</p>;
if (error) return <p>{error.message}</p>;
return (
<div>
<h2>{currentNode?.title ?? currentNode?.name}</h2>
{fields.map(f => (
<label key={f.name}>
{f.label ?? f.name}
<input
value={String(values[f.name] ?? '')}
onChange={e => setValue(f.name, e.target.value)}
/>
{errors[f.name] && <span className="err">{errors[f.name]}</span>}
</label>
))}
{availableActions.map(a => (
<button key={a.id} onClick={() => transition(a.action)}>
{a.label ?? a.action}
</button>
))}
</div>
);
}The hook also resolves SQL-backed dropdown options for you, so a custom UI does not have to duplicate that logic.
📡 Client: WorkflowClient
A pure TypeScript class with no React dependency.
const client = new WorkflowClient({
apiUrl: 'http://localhost:5035/api/workflow',
headers: { 'X-Tenant-Id': 't1' },
onError: (e) => console.error(e),
});
client.setAuthToken('Bearer …');
client.clearAuthToken();
client.setApiUrl('https://prod.example.com/api/workflow');| Group | Method | Backend route |
|---|---|---|
| Definitions | getDefinitions(tenantId?) | GET /definitions |
| | getDefinition(id) | GET /definitions/{id} |
| | createDefinition(def) | POST /definitions |
| | updateDefinition(id, def) | PUT /definitions/{id} |
| | deleteDefinition(id) | DELETE /definitions/{id} |
| Instances | createInstance(defId, data?, userId?) | POST /instances |
| | getInstance(id) | GET /instances/{id} |
| | getState(id) | GET /instances/{id}/state |
| | transition(id, action, data?, userId?, comment?) | POST /instances/{id}/transition |
| | validate(id, data) | POST /instances/{id}/validate |
| | getFieldOptions(id, fieldName) | GET /instances/{id}/field-options/{fieldName} |
| | cancel(id, request?) | POST /instances/{id}/cancel |
| Admin | getNotificationAdminSettings() | GET /admin/notification-settings |
| | updateNotificationAdminSettings(body) | PUT /admin/notification-settings |
Errors are surfaced as WorkflowClientError with message, status, and an optional per-field errors map. A 503 from the admin endpoints means the host has not enabled the AppSettings persistence layer.
🔠 TypeScript types
All public types live in src/types/index.ts and mirror the backend models exactly:
- Enums —
NodeType,FieldType,FormType,WorkflowStatus,RuleType,ButtonStyle. - Models —
Field,FieldOption,Transition,Rule,RuleMapping,Node,WorkflowDefinition,WorkflowInstance,HistoryEntry,WorkflowState. - Designer metadata —
WorkflowActionInfo,WorkflowInputProperty,WorkflowOutputProperty,WorkflowGuardInfo,WorkflowRuleInfo. - Admin —
WorkflowNotificationAdminSettings. - Client —
WorkflowClientOptions,CreateInstanceRequest,TransitionRequest,CancelRequest,ValidationResult.
import type {
WorkflowDefinition, Node, Transition, Field, WorkflowState,
WorkflowActionInfo, WorkflowGuardInfo,
} from 'qp-workflow-react';📝 Field types & validation
The runner supports 18 field types (the 15 backend enum values plus typed text variants):
Text, TextArea, Number, Decimal, Date, DateTime, Time, Checkbox, Radio, Dropdown, MultiSelect, File, Image, RichText, Hidden, Email, Phone, Currency.
Client-side validation applied by WorkflowRunner and useWorkflow:
required,min/maxfor numeric types andmaxLengthfor text,validationPattern(regex) with optionalvalidationMessage,- typed checks: numeric parsing, ISO date / time parsing, email / phone format, currency parsing.
If validation fails, the form keeps the user's values, sets errors[name], and skips the API call.
🔍 SQL-backed dropdown options
Designer fields can carry an optionsQuery instead of static options. The runtime never receives that SQL — both WorkflowRunner and useWorkflow resolve options at load time through GET /instances/{id}/field-options/{fieldName}.
The client normalizes the response so that:
valueandlabelare accepted in either casing,- a blank
labelfalls back to the stringifiedvalue, - objects with
id/namekeys are mapped tovalue/labelautomatically.
🔗 External forms
If a node's formType === 'External', the form can live anywhere (legacy MVC page, third-party app). Helpers:
buildExternalFormUrl(baseUrl, instance, node, options?)— appendsworkflowInstanceId,workflowNodeId,workflowDefinitionId,workflowActions, optionalworkflowCallback, and anyextraParams.parseWorkflowParams(searchParams?)/useWorkflowParams()— read those params back inside the external page.useWorkflowExternalForm(...)— convenience hook that wrapsWorkflowClientand exposes asubmit(action, formData)helper plusgoBack()to the callback URL.
import { useWorkflowParams, WorkflowClient } from 'qp-workflow-react';
function MyExternalForm() {
const { instanceId, actions, callbackUrl } = useWorkflowParams();
const client = new WorkflowClient({ apiUrl: '/api/workflow' });
async function handleSubmit(action: string) {
await client.transition(instanceId!, action, { reason: '…' });
if (callbackUrl) window.location.href = callbackUrl;
}
return (
<form>
{actions.includes('approve') && <button onClick={() => handleSubmit('approve')}>Approve</button>}
{actions.includes('reject') && <button onClick={() => handleSubmit('reject')}>Reject</button>}
</form>
);
}🌍 Localization
import { LocaleProvider, useLocale, getLocale, locales } from 'qp-workflow-react';
const t = getLocale('tr');
t.common.save; // "Kaydet"
t.actions.approve; // "Onayla"Pass locale="tr" to TreeDesigner, WorkflowRunner, and the host modal, or wrap your tree in <LocaleProvider> so descendant components pick the language up via useLocale(). Available codes today are 'en' and 'tr'; add new ones by exporting a Locale object from src/locales.
📡 Backend contract
This package is built against the Chd.Workflow REST contract:
| Method | Path |
|---|---|
| GET | /definitions, /definitions/{id} |
| POST | /definitions |
| PUT | /definitions/{id} |
| DELETE | /definitions/{id} |
| POST | /instances |
| GET | /instances/{id}, /instances/{id}/state |
| GET | /instances/{id}/field-options/{fieldName} |
| POST | /instances/{id}/transition, /instances/{id}/validate, /instances/{id}/cancel |
| GET | /actions, /actions/grouped, /guards, /rules |
| GET / PUT | /admin/notification-settings |
| GET | /admin/participant-groups |
If you bring your own backend, mirror this surface (camelCase JSON, JsonStringEnumConverter-style enums) and the components work without changes.
🛠 Build & publish
npm install
npm run build # tsup → dist/index.js + dist/index.mjs + dist/index.d.ts
npm publish # add --access public for scoped/public packagesImportant package.json fields to keep in sync:
name,version,description,repository,license,main,module,types,files(typicallydist,README.md,LICENSE),peerDependenciesshould pin React 18+.
🩺 Troubleshooting
- Designer action dropdown is empty → the backend has not scanned actions. Call
services.AddWorkflowActionsFromCallingAssembly()(orAddWorkflowActions(typeof(MyAction).Assembly)) beforeAddWorkflow(). Savereturns 400 with an enum error → the host bypassedAddWorkflow()and the minimal-API JSON binder is missingJsonStringEnumConverter. Use the package's registration extensions.- Dropdown is empty even though
optionsQueryis set → checkGET /instances/{id}/field-options/{fieldName}from the browser dev tools; the SQL must beSELECT-only and return eithervalue/labelorid/namecolumns. - CORS errors → enable CORS for the frontend origin on the backend (development presets like
AllowAnyOriginshould not be used in production). /admin/notification-settingsreturns 503 → the host has not registered the AppSettings persistence service; install / enableWorkflowAppSettingsPersistenceon the backend.- External form callback never fires → confirm
workflowCallbackis included in the URL and that the external page callsclient.transitionwith the rightinstanceIdfromuseWorkflowParams().
📦 Related packages
| Package | Platform | Description | |---|---|---| | Chd.Workflow | NuGet | .NET workflow engine | | qp-workflow-react | npm | React components for workflow design & execution (this package) |
📄 License
MIT License — see LICENSE.
Made with ❤️ for the React + .NET community
