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

@erpsquad/approval-workflow

v1.1.0

Published

Module-agnostic approval workflow builder and approval dashboard for ERPForce modules

Readme

@erpsquad/approval-workflow

Approval workflow builder and approval dashboard for ERPForce modules.

The package is module-agnostic: it knows about workflows, criteria, approvers and approval requests, and nothing about leave requests, purchase orders or payroll runs. Modules plug themselves in through configuration and adapters, so Procurement, HRMS, Finance and Inventory can all consume the same build.

Procurement → Purchase Order Approval
Sales       → Sales Order Approval
HRMS        → Leave Approval
Finance     → Payment Approval
Inventory   → Stock Adjustment Approval

Contents

| Document | What it covers | | --- | --- | | docs/ANALYSIS.md | Analysis of the original HRMS implementation this was extracted from | | docs/ARCHITECTURE.md | Folder structure and the responsibility of each layer | | docs/INTEGRATION.md | Step-by-step integration guide | | docs/API.md | Full public API reference | | docs/PERFORMANCE.md | Bundle and runtime performance review | | docs/MIGRATION.md | Adopting the package without touching the existing implementation |


Installation

npm install @erpsquad/approval-workflow

Peer dependencies

Everything shared with the host application is a peer dependency, so there is exactly one instance of React, MUI, Emotion and @erpsquad/common in the final bundle. The package itself has zero runtime dependencies.

| Peer | Range | Required | | --- | --- | --- | | react, react-dom | ^18.2 or ^19 | yes | | @mui/material, @mui/icons-material | ^5.15 | yes | | @emotion/react, @emotion/styled | ^11 | yes | | @erpsquad/common | >=1.11 | yes | | react-hook-form | ^7.50 | optional — workflow form, …/schemas and the root entry | | @hookform/resolvers, yup | ^3 / ^1 | optional — as above | | @xyflow/react | ^12 | optional — only for the workflow builder |

The optional peers are optional on purpose: a module that only renders the approval dashboard or the approval-history button loads none of them, and npm does not install duplicate copies for it. A module that renders the workflow screens, or imports from the root entry, needs all of them. See docs/PERFORMANCE.md.

The package ships ESM (.esm.js) and CommonJS (.cjs) builds with type declarations for every entry point.


Quick start

1. Mount the provider

import {
  ApprovalWorkflowProvider,
  createFetchHttpClient,
  createModuleRegistry,
  createRouteAdapters,
} from '@erpsquad/approval-workflow';

const http = createFetchHttpClient({
  baseUrls: {
    default: env.GATEWAY_URL,
    workflowAutomation: `${env.GATEWAY_URL}/workflow-automation`,
    systemFeature: `${env.GATEWAY_URL}/system-feature`,
    rbac: `${env.GATEWAY_URL}/rbac`,
    organization: `${env.GATEWAY_URL}/hrms`,
  },
  getHeaders: () => getToken(),
});

const modules = createModuleRegistry(
  createRouteAdapters({
    leave_requests: { route: '/hrms/leave-requests/:id', title: 'Leave Request' },
    asset_requests: { route: '/hrms/asset-requests/:id', title: 'Asset Request' },
  }),
);

function ApprovalsRoot() {
  const navigate = useNavigate();
  const { user } = useAuth();

  return (
    <ApprovalWorkflowProvider
      http={http}
      moduleContext={{ module: 'hrms' }}
      currentUser={{ id: user.id, company_id: user.company_id }}
      routes={{
        workflowList: '/hrms/workflows',
        workflowCreate: '/hrms/workflows/new',
        workflowEdit: '/hrms/workflows/:id/edit',
        workflowView: '/hrms/workflows/:id',
        approvalDashboard: '/hrms/approvals',
        approvalView: '/hrms/approvals/:id',
      }}
      navigate={navigate}
      modules={modules}
      notify={(message, variant) => enqueueSnackbar(message, { variant })}
      translate={(key, fallback) => t(key, { defaultValue: fallback })}
    >
      <Outlet />
    </ApprovalWorkflowProvider>
  );
}

2. Render the screens

import { WorkflowListView, WorkflowFormView, WorkflowDetailView }
  from '@erpsquad/approval-workflow/views/workflow';
import { ApprovalDashboardView, ApprovalRequestDetailView }
  from '@erpsquad/approval-workflow/views/dashboard';

const routes = [
  { path: '/hrms/workflows',           element: <WorkflowListView /> },
  { path: '/hrms/workflows/new',       element: <WorkflowFormView mode="create" /> },
  { path: '/hrms/workflows/:id/edit',  element: <EditRoute /> },
  { path: '/hrms/workflows/:id',       element: <ViewRoute /> },
  { path: '/hrms/approvals',           element: <ApprovalDashboardView /> },
  { path: '/hrms/approvals/:id',       element: <ApprovalDetailRoute /> },
];

// The package does not own the router, so the id comes from the host.
const EditRoute = () => {
  const { id } = useParams();
  return <WorkflowFormView mode="edit" workflowId={id} />;
};

That is a working integration. Everything below is refinement.


Integration contracts

| Contract | Prop | Default | | --- | --- | --- | | HTTP transport | http | — required | | Owning module | moduleContext | — required | | Signed-in user | currentUser | — required | | Routes | routes | — required | | Navigation | navigate | — required | | Module adapters | modules | generic adapter only | | Tenant / company scope | tenant | none | | Translation | translate | bundled English fallbacks | | Notifications | notify | console.warn | | Error reporting | onError | none | | List/table state | listState | local component state | | Server-state cache | queryClient | one per provider | | Entity catalogue | entityCatalogue | fetched from /v1/user/modules |

Nothing else couples the package to a host. There are no environment variables, no global store, and no hard-coded URL or route anywhere in src/. A missing http, navigate, routes or moduleContext.module throws an ApprovalWorkflowError (code: 'invalid-config') on mount rather than failing later inside a hook.

Errors

Users only ever see a safe message: the server's text for a client error (4xx), the translated "Something went wrong" for a server fault, a network failure or a bug. The full error, with status, code, server details and the original cause, goes to onError:

<ApprovalWorkflowProvider
  // …
  onError={(error, context) => Sentry.captureException(error, { extra: context })}
>

context is { source: 'query', key } for a failed read, or { source: 'mutation', operation } for a failed write (workflow.save, workflow.remove, approval.decision, approval.preAction).

Sharing one cache between providers

import { createQueryCache } from '@erpsquad/approval-workflow/hooks';

const approvalCache = createQueryCache();
// Pass `queryClient={approvalCache}` to every provider that should share
// responses, e.g. the dashboard route and a record page's history button.

Adding a module

The registry is how an ERP module joins the approval dashboard. The minimum is a route:

createRouteAdapters({
  purchase_orders: { route: '/procurement/orders/:id', title: 'Purchase Order' },
});

A module with its own approval API and modal supplies them:

defineModuleAdapter('loan_requests', {
  title: 'Loan Request',
  getRecordRoute: (request) => `/hrms/loans/${request.entity_id}`,
  modalComponent: LoanApprovalModal,
  getModalProps: (state, onConfirm, onClose) => ({
    open: state.open,
    onClose,
    onConfirm,
    type: state.decision?.toLowerCase(),
    data: state.request?.request_data,
  }),
  submitDecision: ({ entityId, decision, formData }) =>
    dispatch(patchLoanRequestStatus({ id: entityId, status: decision, payload: formData })),
});

getModalProps is passed through verbatim, so existing modals work unchanged — see docs/MIGRATION.md.


Permissions

Nothing to configure. The package reads the ERP's global permission state itself, from usePermissions() in @erpsquad/common — the same hook every other ERPForce screen uses. Just render the screens under the app's PermissionsProvider, which in an ERPForce app is already the case.

| Area | RBAC key | Behaviour | | --- | --- | --- | | Approval Workflow | WorkflowAutomation | access control | | Approval Dashboard | Approvals, else ErpCommonApprovals | access control | | Approval Type → Department | Departments | show / hide | | Approval Type → Hierarchy | OrganisationStructure | show / hide | | Approval Type → User | employee | selects the user data source | | Applicable → Department | Departments | show / hide | | Applicable → Grade | Grades | show / hide | | Applicable → Employee Type | employee | show / hide |

Hidden options are removed, not disabled. The User approval type is always offered: with employee it reads the employee directory, without it the users API — storing the same user ids either way.

Read the resolved capabilities yourself when your route guards or menus need the same answers:

import { useApprovalPermissions } from '@erpsquad/approval-workflow/permissions';

const permissions = useApprovalPermissions();
permissions['workflow.view'];
permissions['approval.approve'];

See docs/API.md for the full capability matrix.


Entry points

Import from the narrowest entry that covers your need.

| Entry | Contents | | --- | --- | | @erpsquad/approval-workflow | everything | | …/views | all screens | | …/views/workflow | workflow list, form, detail | | …/views/dashboard | approval dashboard and detail (no React Flow) | | …/components | feedback, modals, chips, list primitives | | …/components/builder | the canvas, nodes, edges, sidebars | | …/components/approval-history | the Approval History button and popover, for any record page | | …/hooks | data hooks, no UI | | …/providers | provider and config hooks | | …/api | transport and endpoint clients | | …/services | workflow and approval business logic | | …/adapters | module registry | | …/permissions | capability hooks, gate, RBAC keys | | …/schemas | yup schemas | | …/types | types only (erased at build time) | | …/constants, …/utils | field names, options, pure helpers |


Scripts

npm run build            # dual ESM + CJS build with declarations
npm run type-check       # tsc --noEmit, strict
npm run lint             # eslint, zero warnings
npm test                 # vitest
npm run validate         # lint + type-check + test
npm run validate:package # post-build gate (exports, externals, no leaked src)

License

MIT