npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

qp-workflow-react

v1.0.4

Published

React components and hooks for visual workflow design and execution. Pairs with Chd.Workflow .NET backend.

Readme

qp-workflow-react 🎨

npm TypeScript React License: MIT

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?

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}/state on 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),
  • requiresConfirmation and requiresComment modals,
  • resolving SQL-backed optionsQuery dropdowns 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-react

Peer 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 /actions and GET /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 (when enableFormEditor is on) supports all 18 field types and edits optionsQuery, 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}/state on 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) and transition.requiresComment (comment textarea modal).
  • Detects node.formType === 'External' and uses buildExternalFormUrl to redirect with workflow context query params.
  • For Dropdown / Radio / MultiSelect fields whose definition contains an optionsQuery, options are fetched via GET /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:

  • EnumsNodeType, FieldType, FormType, WorkflowStatus, RuleType, ButtonStyle.
  • ModelsField, FieldOption, Transition, Rule, RuleMapping, Node, WorkflowDefinition, WorkflowInstance, HistoryEntry, WorkflowState.
  • Designer metadataWorkflowActionInfo, WorkflowInputProperty, WorkflowOutputProperty, WorkflowGuardInfo, WorkflowRuleInfo.
  • AdminWorkflowNotificationAdminSettings.
  • ClientWorkflowClientOptions, 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 / max for numeric types and maxLength for text,
  • validationPattern (regex) with optional validationMessage,
  • 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:

  • value and label are accepted in either casing,
  • a blank label falls back to the stringified value,
  • objects with id / name keys are mapped to value / label automatically.

🔗 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?) — appends workflowInstanceId, workflowNodeId, workflowDefinitionId, workflowActions, optional workflowCallback, and any extraParams.
  • parseWorkflowParams(searchParams?) / useWorkflowParams() — read those params back inside the external page.
  • useWorkflowExternalForm(...) — convenience hook that wraps WorkflowClient and exposes a submit(action, formData) helper plus goBack() 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 packages

Important package.json fields to keep in sync:

  • name, version, description, repository, license,
  • main, module, types,
  • files (typically dist, README.md, LICENSE),
  • peerDependencies should pin React 18+.

🩺 Troubleshooting

  • Designer action dropdown is empty → the backend has not scanned actions. Call services.AddWorkflowActionsFromCallingAssembly() (or AddWorkflowActions(typeof(MyAction).Assembly)) before AddWorkflow().
  • Save returns 400 with an enum error → the host bypassed AddWorkflow() and the minimal-API JSON binder is missing JsonStringEnumConverter. Use the package's registration extensions.
  • Dropdown is empty even though optionsQuery is set → check GET /instances/{id}/field-options/{fieldName} from the browser dev tools; the SQL must be SELECT-only and return either value / label or id / name columns.
  • CORS errors → enable CORS for the frontend origin on the backend (development presets like AllowAnyOrigin should not be used in production).
  • /admin/notification-settings returns 503 → the host has not registered the AppSettings persistence service; install / enable WorkflowAppSettingsPersistence on the backend.
  • External form callback never fires → confirm workflowCallback is included in the URL and that the external page calls client.transition with the right instanceId from useWorkflowParams().

📦 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