PSA/docs/workflow/integration-patterns.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

47 KiB

Integration Patterns: Dynamic Workflow UI System

Overview

The Dynamic Workflow UI System is designed to be extensible and adaptable to various business needs. This document outlines the integration patterns for extending the system with custom functionality, integrating with external systems, and building on top of the core components.

Important Note on Workflow Execution: Workflows are executed in response to events. When a workflow is triggered, the event that triggered it is passed as input to the workflow. The workflow does not wait for the initial event - the fact that the workflow is executing means the event has already occurred. This is reflected in the patterns below where each workflow receives its triggering event via context.input.triggerEvent.

Table of Contents

  1. Extending the Form System

  2. Extending the Action System

  3. Workflow Integration Patterns

  4. External System Integration

  5. Advanced Extension Patterns

Extending the Form System

Custom Widgets

The Dynamic Form system can be extended with custom widgets to support specialized input types.

Creating a Custom Widget

import { WidgetProps } from '@rjsf/utils';

// Custom widget for a specialized input type
export const MyCustomWidget = (props: WidgetProps) => {
  const { id, value, onChange, disabled, readonly, placeholder } = props;
  
  return (
    <div className="my-custom-widget">
      <input
        id={id}
        value={value || ''}
        disabled={disabled || readonly}
        placeholder={placeholder}
        onChange={(e) => onChange(e.target.value)}
        // Additional custom properties
      />
      {/* Additional custom UI elements */}
    </div>
  );
};

// Add the widget to the customWidgets object
export const customWidgets = {
  // ... existing widgets
  MyCustomWidget,
};

Registering a Custom Widget

To make your custom widget available to the form system, add it to the customWidgets object in customWidgets.tsx:

// In customWidgets.tsx
export const customWidgets = {
  CompanyPickerWidget,
  InputWidget,
  TextAreaWidget,
  DatePickerWidget,
  UserPickerWidget,
  CheckboxWidget,
  // Add your custom widget
  MyCustomWidget,
};

Using a Custom Widget in UI Schema

{
  "myField": {
    "ui:widget": "MyCustomWidget",
    "ui:options": {
      // Custom options for your widget
    }
  }
}

Custom Field Templates

Field templates control the layout and structure of form fields. You can create custom field templates to customize the appearance of form fields.

Creating a Custom Field Template

import { FieldTemplateProps } from '@rjsf/utils';

export function MyCustomFieldTemplate(props: FieldTemplateProps) {
  const {
    id,
    label,
    help,
    required,
    description,
    errors,
    children,
  } = props;
  
  return (
    <div className="my-custom-field">
      <label htmlFor={id}>
        {label}
        {required && <span className="required">*</span>}
      </label>
      {description && <div className="field-description">{description}</div>}
      {children}
      {errors && <div className="field-error">{errors}</div>}
      {help && <div className="field-help">{help}</div>}
    </div>
  );
}

Using a Custom Field Template

import { withTheme } from '@rjsf/core';
import { MyCustomFieldTemplate } from './MyCustomFieldTemplate';

const ThemedForm = withTheme({});

function MyForm() {
  return (
    <ThemedForm
      schema={schema}
      uiSchema={uiSchema}
      templates={{ FieldTemplate: MyCustomFieldTemplate }}
    />
  );
}

Custom Validation

You can extend the form validation system with custom validation logic.

Creating a Custom Validator

import Ajv from 'ajv';
import addFormats from 'ajv-formats';

export function createCustomValidator() {
  const ajv = new Ajv({
    allErrors: true,
    multipleOfPrecision: 8,
    strict: false,
    strictTypes: false,
    strictTuples: false,
  });
  
  // Add formats
  addFormats(ajv);
  
  // Add custom formats
  ajv.addFormat('custom-email', /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/);
  
  // Add custom keywords
  ajv.addKeyword({
    keyword: 'customValidation',
    validate: (schema: any, data: any) => {
      // Custom validation logic
      return true; // or false if validation fails
    },
    errors: true,
  });
  
  return ajv;
}

Using a Custom Validator

import { withTheme } from '@rjsf/core';
import { createCustomValidator } from './customValidator';

const ThemedForm = withTheme({});
const customValidator = createCustomValidator();

function MyForm() {
  return (
    <ThemedForm
      schema={schema}
      uiSchema={uiSchema}
      validator={customValidator}
    />
  );
}

Conditional Logic Extensions

You can extend the conditional logic system to support more complex conditions.

Creating Custom Condition Types

// In conditionalLogic.ts
function evaluateDisplayCondition(
  condition: any,
  formData: Record<string, any>
): boolean {
  // ... existing condition types
  
  // Add custom condition type
  if (condition.type === 'custom') {
    // Implement custom condition logic
    return evaluateCustomCondition(condition, formData);
  }
  
  // Default to showing the field if the condition is not recognized
  return true;
}

function evaluateCustomCondition(
  condition: any,
  formData: Record<string, any>
): boolean {
  // Implement custom condition logic
  // For example, a condition that checks if a field matches a regex pattern
  if (condition.field && condition.pattern) {
    const fieldValue = formData[condition.field];
    const regex = new RegExp(condition.pattern);
    return regex.test(fieldValue);
  }
  
  return true;
}

Using Custom Condition Types

{
  "myField": {
    "ui:displayIf": {
      "type": "custom",
      "field": "otherField",
      "pattern": "^[A-Z].*$"
    }
  }
}

Extending the Action System

Custom Action Handlers

The Action System can be extended with custom action handlers to support specialized actions.

Creating a Custom Action Handler

import { actionHandlerRegistry, ActionHandlerContext, ActionHandlerResult } from '../../lib/workflow/forms/actionHandlerRegistry';

// Register a custom action handler
actionHandlerRegistry.registerHandler(
  'myCustomAction',
  async (action, context: ActionHandlerContext): Promise<ActionHandlerResult> => {
    try {
      // Extract data from context
      const { formData, taskId, executionId, contextData } = context;
      
      // Implement custom action logic
      console.log('Executing custom action:', action.id);
      console.log('Form data:', formData);
      
      // For example, call an external API
      const result = await callExternalApi(formData);
      
      return {
        success: true,
        message: 'Custom action executed successfully',
        data: result
      };
    } catch (error) {
      console.error('Error executing custom action:', error);
      return {
        success: false,
        message: error instanceof Error ? error.message : 'An error occurred'
      };
    }
  }
);

// Helper function for the example
async function callExternalApi(data: any) {
  // Implementation of API call
  return { status: 'success', timestamp: new Date().toISOString() };
}

Using a Custom Action Handler

// In your component
const actions = [
  {
    id: 'myCustomAction',
    label: 'Execute Custom Action',
    primary: true,
    variant: 'default',
    disabled: false,
    hidden: false,
    order: 0
  }
];

function MyComponent() {
  return (
    <DynamicForm
      schema={schema}
      uiSchema={uiSchema}
      formData={formData}
      actions={actions}
    />
  );
}

Action Button Customization

You can customize the appearance and behavior of action buttons.

Creating Custom Action Buttons

import { Action } from '../../lib/workflow/forms/actionHandlerRegistry';
import { Button } from '../ui/Button';

interface CustomActionButtonProps {
  action: Action;
  onClick: () => void;
  isSubmitting: boolean;
}

export function CustomActionButton({ action, onClick, isSubmitting }: CustomActionButtonProps) {
  return (
    <Button
      variant={action.variant}
      disabled={action.disabled || isSubmitting}
      onClick={onClick}
      className="custom-action-button"
    >
      {action.icon && <span className={`icon ${action.icon}`} />}
      {action.label}
      {isSubmitting && action.primary && <span className="spinner" />}
    </Button>
  );
}

Using Custom Action Buttons

import { ActionButtonGroup } from './ActionButtonGroup';
import { CustomActionButton } from './CustomActionButton';

function MyComponent() {
  return (
    <ActionButtonGroup
      actions={actions}
      onAction={handleAction}
      isSubmitting={isSubmitting}
      buttonComponent={CustomActionButton}
    />
  );
}

Action Context Extensions

You can extend the action context to provide additional data to action handlers.

Extending the Action Handler Context

// In actionHandlerRegistry.ts
export interface ActionHandlerContext {
  formData: any;
  taskId?: string;
  executionId?: string;
  contextData?: Record<string, any>;
  // Add custom context properties
  currentUser?: {
    id: string;
    name: string;
    roles: string[];
  };
  tenant?: string;
  customData?: Record<string, any>;
}

Providing Extended Context

function MyComponent() {
  // Get current user information
  const currentUser = useCurrentUser();
  const tenant = useTenant();
  
  // Create extended context
  const extendedContext = {
    currentUser,
    tenant,
    customData: {
      // Additional custom data
    }
  };
  
  return (
    <DynamicForm
      schema={schema}
      uiSchema={uiSchema}
      formData={formData}
      actions={actions}
      actionContext={extendedContext}
    />
  );
}

Workflow Integration Patterns

Creating Human Tasks

Workflows can create human tasks to involve users in the workflow process.

Basic Task Creation

// Within a workflow definition
async function approvalWorkflow(context) {
  // Create a human task
  const { taskId } = await context.actions.createHumanTask({
    taskType: 'approval',
    title: 'Approve Request',
    description: 'Please review and approve this request',
    priority: 'high',
    dueDate: '2 days', // Relative due date
    assignTo: {
      roles: ['manager'],
      users: [] // Optionally assign to specific users
    },
    contextData: {
      requestId: context.data.get('requestId'),
      amount: context.data.get('amount'),
      customerId: context.data.get('customerId')
    }
  });
  
  // Wait for the task to be completed
  const taskComplete = await context.events.waitFor(`Task:${taskId}:Complete`);
  
  // Process the form submission data
  const { approved, comments } = taskComplete.payload;
  
  if (approved) {
    // Handle approval
    context.setState('approved');
  } else {
    // Handle rejection
    context.setState('rejected');
    context.data.set('rejectionReason', comments);
  }
}

Advanced Task Creation Patterns

// Within a workflow definition
async function multiStepApprovalWorkflow(context) {
  // Step 1: Initial review
  const { taskId: initialReviewTaskId } = await context.actions.createHumanTask({
    taskType: 'initial_review',
    title: 'Initial Review',
    assignTo: { roles: ['reviewer'] }
  });
  
  // Wait for initial review
  const initialReviewComplete = await context.events.waitFor(`Task:${initialReviewTaskId}:Complete`);
  
  // Step 2: Manager approval (only if initial review is positive)
  if (initialReviewComplete.payload.recommendation === 'approve') {
    const { taskId: managerApprovalTaskId } = await context.actions.createHumanTask({
      taskType: 'manager_approval',
      title: 'Manager Approval',
      assignTo: { roles: ['manager'] },
      contextData: {
        initialReviewerId: initialReviewComplete.user_id,
        initialReviewComments: initialReviewComplete.payload.comments
      }
    });
    
    // Wait for manager approval
    const managerApprovalComplete = await context.events.waitFor(`Task:${managerApprovalTaskId}:Complete`);
    
    // Process manager decision
    if (managerApprovalComplete.payload.approved) {
      context.setState('approved');
    } else {
      context.setState('rejected_by_manager');
    }
  } else {
    // Initial review was negative
    context.setState('rejected_by_reviewer');
  }
}

Processing Task Responses

Workflows can process the responses from human tasks to make decisions and continue execution.

Basic Response Processing

// Within a workflow definition
async function creditApprovalWorkflow(context) {
  // Create a task
  const { taskId } = await context.actions.createHumanTask({
    taskType: 'credit_approval',
    title: 'Approve Credit Request',
    // ... other task properties
  });
  
  // Wait for task completion
  const taskComplete = await context.events.waitFor(`Task:${taskId}:Complete`);
  
  // Process the response
  const { approved, amount, reason } = taskComplete.payload;
  
  if (approved) {
    // Update credit limit
    await context.actions.updateCreditLimit({
      customerId: context.data.get('customerId'),
      newLimit: amount
    });
    
    // Send approval notification
    await context.actions.sendNotification({
      recipient: context.data.get('customerEmail'),
      template: 'credit_approval',
      data: {
        amount,
        reason
      }
    });
    
    context.setState('approved');
  } else {
    // Send rejection notification
    await context.actions.sendNotification({
      recipient: context.data.get('customerEmail'),
      template: 'credit_rejection',
      data: {
        reason
      }
    });
    
    context.setState('rejected');
  }
}

Advanced Response Processing

// Within a workflow definition
async function complexApprovalWorkflow(context) {
  // Create a task with a complex form
  const { taskId } = await context.actions.createHumanTask({
    taskType: 'complex_approval',
    title: 'Complex Approval',
    // ... other task properties
  });
  
  // Wait for task completion
  const taskComplete = await context.events.waitFor(`Task:${taskId}:Complete`);
  
  // Extract response data
  const {
    decision,
    adjustments,
    comments,
    additionalApprovers,
    attachments
  } = taskComplete.payload;
  
  // Process decision
  switch (decision) {
    case 'approve':
      // Process approval
      await processApproval(context, adjustments);
      break;
    case 'reject':
      // Process rejection
      await processRejection(context, comments);
      break;
    case 'escalate':
      // Process escalation
      await processEscalation(context, additionalApprovers);
      break;
    case 'request_more_info':
      // Process request for more information
      await processInfoRequest(context, comments);
      break;
    default:
      throw new Error(`Unknown decision: ${decision}`);
  }
  
  // Process attachments if any
  if (attachments && attachments.length > 0) {
    await processAttachments(context, attachments);
  }
}

// Helper functions for the example
async function processApproval(context, adjustments) {
  // Implementation
}

async function processRejection(context, comments) {
  // Implementation
}

async function processEscalation(context, additionalApprovers) {
  // Implementation
}

async function processInfoRequest(context, comments) {
  // Implementation
}

async function processAttachments(context, attachments) {
  // Implementation
}

Custom Event Patterns

You can define custom event patterns for workflow-task integration.

Custom Event Naming Conventions

// Event naming conventions
const EVENT_PATTERNS = {
  TASK_CREATED: 'Task:{taskId}:Created',
  TASK_ASSIGNED: 'Task:{taskId}:Assigned',
  TASK_CLAIMED: 'Task:{taskId}:Claimed',
  TASK_UNCLAIMED: 'Task:{taskId}:Unclaimed',
  TASK_COMPLETED: 'Task:{taskId}:Complete',
  TASK_REJECTED: 'Task:{taskId}:Reject',
  TASK_CANCELED: 'Task:{taskId}:Cancel',
  TASK_EXPIRED: 'Task:{taskId}:Expired',
  TASK_REASSIGNED: 'Task:{taskId}:Reassigned',
  FORM_SUBMITTED: 'Form:{formId}:Submit',
  FORM_SAVED: 'Form:{formId}:Save',
  FORM_VALIDATED: 'Form:{formId}:Validate'
};

// Usage in workflow
async function workflowWithCustomEvents(context) {
  // Create a task
  const { taskId } = await context.actions.createHumanTask({
    // ... task properties
  });
  
  // Wait for multiple possible events
  const event = await context.events.waitFor([
    EVENT_PATTERNS.TASK_COMPLETED.replace('{taskId}', taskId),
    EVENT_PATTERNS.TASK_REJECTED.replace('{taskId}', taskId),
    EVENT_PATTERNS.TASK_EXPIRED.replace('{taskId}', taskId)
  ]);
  
  // Process the event based on its name
  switch (event.name) {
    case EVENT_PATTERNS.TASK_COMPLETED.replace('{taskId}', taskId):
      // Process completion
      break;
    case EVENT_PATTERNS.TASK_REJECTED.replace('{taskId}', taskId):
      // Process rejection
      break;
    case EVENT_PATTERNS.TASK_EXPIRED.replace('{taskId}', taskId):
      // Process expiration
      break;
  }
}

Custom Event Payloads

// Custom event payload structure
interface TaskCompletionPayload {
  decision: 'approve' | 'reject' | 'escalate';
  comments: string;
  metadata: {
    completedAt: string;
    completedBy: string;
    clientInfo: {
      browser: string;
      os: string;
      ip: string;
    };
  };
  formData: Record<string, any>;
}

// Creating an event with custom payload
async function createTaskCompletionEvent(
  taskId: string,
  executionId: string,
  payload: TaskCompletionPayload,
  userId: string,
  tenant: string
) {
  return workflowRuntime.enqueueEvent({
    execution_id: executionId,
    event_name: `Task:${taskId}:Complete`,
    event_type: 'task_completed',
    payload,
    user_id: userId,
    tenant,
    metadata: {
      source: 'task_inbox',
      timestamp: new Date().toISOString()
    }
  });
}

Workflow State Management

Workflows can manage state based on task interactions.

State Transitions Based on Task Outcomes

// Within a workflow definition
async function stateBasedWorkflow(context) {
  // Initial state - Processing
  context.setState('processing');
  
  // The workflow is triggered by a Submit event, which is passed as input
  const { triggerEvent } = context.input;
  logger.info(`Processing submission from ${triggerEvent.user_id}`);
  context.setState('submitted');
  
  // Create review task
  const { taskId } = await context.actions.createHumanTask({
    taskType: 'review',
    title: 'Review Submission',
    // ... other task properties
  });
  
  // Wait for review completion
  const reviewComplete = await context.events.waitFor(`Task:${taskId}:Complete`);
  
  // Transition state based on review outcome
  if (reviewComplete.payload.decision === 'approve') {
    context.setState('approved');
    
    // Create processing task
    const { taskId: processingTaskId } = await context.actions.createHumanTask({
      taskType: 'processing',
      title: 'Process Approved Submission',
      // ... other task properties
    });
    
    // Wait for processing completion
    await context.events.waitFor(`Task:${processingTaskId}:Complete`);
    context.setState('processed');
  } else if (reviewComplete.payload.decision === 'reject') {
    context.setState('rejected');
  } else if (reviewComplete.payload.decision === 'revise') {
    context.setState('revision_requested');
    
    // The workflow will end here, and a new workflow will be triggered when a Resubmit event occurs
    logger.info('Waiting for document resubmission');
    
    // Note: In a real implementation, we would end this workflow here
    // and start a new workflow instance when the Resubmit event occurs
  }
}

Parallel State Management

// Within a workflow definition
async function parallelStateWorkflow(context) {
  // Initial state
  context.setState('initiated');
  
  // Create multiple parallel tasks
  const [
    { taskId: financialReviewTaskId },
    { taskId: technicalReviewTaskId },
    { taskId: legalReviewTaskId }
  ] = await Promise.all([
    context.actions.createHumanTask({
      taskType: 'financial_review',
      title: 'Financial Review',
      // ... other task properties
    }),
    context.actions.createHumanTask({
      taskType: 'technical_review',
      title: 'Technical Review',
      // ... other task properties
    }),
    context.actions.createHumanTask({
      taskType: 'legal_review',
      title: 'Legal Review',
      // ... other task properties
    })
  ]);
  
  // Track review states
  const reviewStates = {
    financial: null,
    technical: null,
    legal: null
  };
  
  // Wait for all reviews to complete
  await Promise.all([
    (async () => {
      const financialReview = await context.events.waitFor(`Task:${financialReviewTaskId}:Complete`);
      reviewStates.financial = financialReview.payload.approved ? 'approved' : 'rejected';
      context.data.set('financialReviewComments', financialReview.payload.comments);
    })(),
    (async () => {
      const technicalReview = await context.events.waitFor(`Task:${technicalReviewTaskId}:Complete`);
      reviewStates.technical = technicalReview.payload.approved ? 'approved' : 'rejected';
      context.data.set('technicalReviewComments', technicalReview.payload.comments);
    })(),
    (async () => {
      const legalReview = await context.events.waitFor(`Task:${legalReviewTaskId}:Complete`);
      reviewStates.legal = legalReview.payload.approved ? 'approved' : 'rejected';
      context.data.set('legalReviewComments', legalReview.payload.comments);
    })()
  ]);
  
  // Determine final state based on all reviews
  const allApproved = Object.values(reviewStates).every(state => state === 'approved');
  
  if (allApproved) {
    context.setState('all_approved');
  } else {
    context.setState('rejected');
    context.data.set('rejectionReasons', Object.entries(reviewStates)
      .filter(([_, state]) => state === 'rejected')
      .map(([type]) => type)
    );
  }
}

External System Integration

API Integration

You can integrate the workflow system with external APIs.

Action Handler for API Integration

// Register an action handler for API integration
actionHandlerRegistry.registerHandler(
  'callExternalApi',
  async (action, context: ActionHandlerContext): Promise<ActionHandlerResult> => {
    try {
      const { formData } = context;
      
      // Extract API configuration from action options
      const {
        url,
        method = 'POST',
        headers = {},
        bodyFields = [],
        responseMapping = {}
      } = action.options || {};
      
      if (!url) {
        throw new Error('API URL is required');
      }
      
      // Build request body from form data
      const body: Record<string, any> = {};
      
      if (bodyFields.length === 0) {
        // Use all form data if no specific fields are specified
        Object.assign(body, formData);
      } else {
        // Use only specified fields
        bodyFields.forEach(field => {
          if (field in formData) {
            body[field] = formData[field];
          }
        });
      }
      
      // Make API request
      const response = await fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...headers
        },
        body: JSON.stringify(body)
      });
      
      if (!response.ok) {
        throw new Error(`API request failed with status ${response.status}`);
      }
      
      const responseData = await response.json();
      
      // Map response data to result
      const result: Record<string, any> = {};
      
      if (Object.keys(responseMapping).length === 0) {
        // Use all response data if no mapping is specified
        Object.assign(result, responseData);
      } else {
        // Map response fields according to mapping
        Object.entries(responseMapping).forEach(([source, target]) => {
          const value = source.split('.').reduce((obj, key) => obj?.[key], responseData);
          if (value !== undefined) {
            result[target as string] = value;
          }
        });
      }
      
      return {
        success: true,
        message: 'API request successful',
        data: result
      };
    } catch (error) {
      console.error('Error calling external API:', error);
      return {
        success: false,
        message: error instanceof Error ? error.message : 'An error occurred'
      };
    }
  }
);

Using the API Integration Action

// In your component
const apiActions = [
  {
    id: 'callExternalApi',
    label: 'Submit to External System',
    primary: true,
    variant: 'default',
    options: {
      url: 'https://api.example.com/data',
      method: 'POST',
      headers: {
        'Authorization': 'Bearer YOUR_API_KEY'
      },
      bodyFields: ['name', 'email', 'message'],
      responseMapping: {
        'id': 'externalId',
        'status': 'externalStatus',
        'created_at': 'timestamp'
      }
    }
  }
];

function ApiIntegrationForm() {
  return (
    <DynamicForm
      schema={schema}
      uiSchema={uiSchema}
      formData={formData}
      actions={apiActions}
    />
  );
}

Data Source Integration

You can integrate the workflow system with external data sources.

Data Source Widget

import { WidgetProps } from '@rjsf/utils';
import { useState, useEffect } from 'react';

// Widget for selecting data from an external data source
export const ExternalDataWidget = (props: WidgetProps) => {
  const { id, value, onChange, disabled, readonly, options } = props;
  const [items, setItems] = useState<Array<{ id: string; name: string }>>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  // Fetch data from external source
  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const { dataSource, params = {} } = options || {};
        
        if (!dataSource) {
          throw new Error('Data source URL is required');
        }
        
        // Build query parameters
        const queryParams = new URLSearchParams();
        Object.entries(params).forEach(([key, value]) => {
          queryParams.append(key, String(value));
        });
        
        // Fetch data
        const response = await fetch(`${dataSource}?${queryParams.toString()}`);
        
        if (!response.ok) {
          throw new Error(`Data source request failed with status ${response.status}`);
        }
        
        const data = await response.json();
        
        // Map data to items
        const mappedItems = data.map((item: any) => ({
          id: item.id,
          name: item.name || item.title || item.label || item.id
        }));
        
        setItems(mappedItems);
      } catch (error) {
        console.error('Error fetching data:', error);
        setError(error instanceof Error ? error.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [options]);
  
  return (
    <div className="external-data-widget">
      {loading && <div className="loading">Loading...</div>}
      {error && <div className="error">{error}</div>}
      
      <select
        id={id}
        value={value || ''}
        disabled={disabled || readonly || loading}
        onChange={(e) => onChange(e.target.value)}
      >
        <option value="">Select an option</option>
        {items.map(item => (
          <option key={item.id} value={item.id}>
            {item.name}
          </option>
        ))}
      </select>
    </div>
  );
};

Using the Data Source Widget

{
  "product": {
    "ui:widget": "ExternalDataWidget",
    "ui:options": {
      "dataSource": "https://api.example.com/products",
      "params": {
        "category": "electronics",
        "limit": 100
      }
    }
  }
}

Authentication Integration

You can integrate the workflow system with authentication systems.

Authentication Context Provider

import { createContext, useContext, useState, useEffect, ReactNode } from 'react';

// Authentication context
interface AuthContext {
  user: {
    id: string;
    name: string;
    roles: string[];
  } | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
}

const AuthContext = createContext<AuthContext>({
  user: null,
  isAuthenticated: false,
  isLoading: true,
  error: null
});

// Authentication provider
interface AuthProviderProps {
  children: ReactNode;
}

export function AuthProvider({ children }: AuthProviderProps) {
  const [user, setUser] = useState<AuthContext['user']>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    const fetchUser = async () => {
      setIsLoading(true);
      setError(null);
      
      try {
        // Fetch user data from authentication system
        const response = await fetch('/api/auth/user');
        
        if (!response.ok) {
          throw new Error(`Authentication request failed with status ${response.status}`);
        }
        
        const userData = await response.json();
        
        setUser({
          id: userData.id,
          name: userData.name,
          roles: userData.roles || []
        });
      } catch (error) {
        console.error('Error fetching user data:', error);
        setError(error instanceof Error ? error.message : 'An error occurred');
        setUser(null);
      } finally {
        setIsLoading(false);
      }
    };
    
    fetchUser();
  }, []);
  
  return (
    <AuthContext.Provider
      value={{
        user,
        isAuthenticated: !!user,
        isLoading,
        error
      }}
    >
      {children}
    </AuthContext.Provider>
  );
}

// Hook for using authentication context
export function useAuth() {
  return useContext(AuthContext);
}

Using Authentication in Task Inbox

import { useAuth } from './AuthProvider';

function TaskInbox() {
  const { user, isAuthenticated, isLoading } = useAuth();
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  if (!isAuthenticated) {
    return <div>Please log in to access the Task Inbox</div>;
  }
  
  return (
    <div>
      <h1>Task Inbox for {user?.name}</h1>
      <TaskList userId={user?.id} userRoles={user?.roles} />
    </div>
  );
}

Notification Integration

You can integrate the workflow system with notification systems.

Notification Action Handler

// Register a notification action handler
actionHandlerRegistry.registerHandler(
  'sendNotification',
  async (action, context: ActionHandlerContext): Promise<ActionHandlerResult> => {
    try {
      const { formData } = context;
      
      // Extract notification configuration from action options
      const {
        type = 'email',
        recipient,
        template,
        subject,
        data = {}
      } = action.options || {};
      
      if (!recipient) {
        throw new Error('Notification recipient is required');
      }
      
      // Combine form data with additional data
      const notificationData = {
        ...formData,
        ...data
      };
      
      // Send notification based on type
      switch (type) {
        case 'email':
          await sendEmailNotification(recipient, template, subject, notificationData);
          break;
        case 'sms':
          await sendSmsNotification(recipient, template, notificationData);
          break;
        case 'push':
          await sendPushNotification(recipient, template, notificationData);
          break;
        default:
          throw new Error(`Unsupported notification type: ${type}`);
      }
      
      return {
        success: true,
        message: `${type} notification sent successfully`
      };
    } catch (error) {
      console.error('Error sending notification:', error);
      return {
        success: false,
        message: error instanceof Error ? error.message : 'An error occurred'
      };
    }
  }
);

// Helper functions for the example
async function sendEmailNotification(recipient: string, template: string, subject: string, data: any) {
  // Implementation of email notification
}

async function sendSmsNotification(recipient: string, template: string, data: any) {
  // Implementation of SMS notification
}

async function sendPushNotification(recipient: string, template: string, data: any) {
  // Implementation of push notification
}

Using the Notification Action

// In your component
const notificationActions = [
  {
    id: 'sendNotification',
    label: 'Submit and Notify',
    primary: true,
    variant: 'default',
    options: {
      type: 'email',
      recipient: 'user@example.com',
      template: 'form_submission',
      subject: 'Form Submission Notification',
      data: {
        applicationId: '12345',
        timestamp: new Date().toISOString()
      }
    }
  }
];

function NotificationForm() {
  return (
    <DynamicForm
      schema={schema}
      uiSchema={uiSchema}
      formData={formData}
      actions={notificationActions}
    />
  );
}

Advanced Extension Patterns

Plugin Architecture

You can implement a plugin architecture to allow for modular extensions of the workflow system.

Plugin Registry

// Plugin interface
interface Plugin {
  id: string;
  name: string;
  version: string;
  initialize: (context: PluginContext) => Promise<void>;
}

// Plugin context
interface PluginContext {
  registerWidget: (name: string, widget: any) => void;
  registerFieldTemplate: (name: string, template: any) => void;
  registerActionHandler: (id: string, handler: any) => void;
  registerValidator: (name: string, validator: any) => void;
  // Add other extension points
}

// Plugin registry
class PluginRegistry {
  private plugins: Map<string, Plugin> = new Map();
  private widgets: Map<string, any> = new Map();
  private fieldTemplates: Map<string, any> = new Map();
  private actionHandlers: Map<string, any> = new Map();
  private validators: Map<string, any> = new Map();
  
  // Register a plugin
  async registerPlugin(plugin: Plugin): Promise<void> {
    if (this.plugins.has(plugin.id)) {
      throw new Error(`Plugin with ID ${plugin.id} is already registered`);
    }
    
    // Create plugin context
    const context: PluginContext = {
      registerWidget: (name, widget) => this.widgets.set(name, widget),
      registerFieldTemplate: (name, template) => this.fieldTemplates.set(name, template),
      registerActionHandler: (id, handler) => this.actionHandlers.set(id, handler),
      registerValidator: (name, validator) => this.validators.set(name, validator)
    };
    
    // Initialize plugin
    await plugin.initialize(context);
    
    // Store plugin
    this.plugins.set(plugin.id, plugin);
    
    console.log(`Plugin ${plugin.name} (${plugin.version}) registered successfully`);
  }
  
  // Get a widget
  getWidget(name: string): any {
    return this.widgets.get(name);
  }
  
  // Get a field template
  getFieldTemplate(name: string): any {
    return this.fieldTemplates.get(name);
  }
  
  // Get an action handler
  getActionHandler(id: string): any {
    return this.actionHandlers.get(id);
  }
  
  // Get a validator
  getValidator(name: string): any {
    return this.validators.get(name);
  }
  
  // Get all widgets
  getAllWidgets(): Record<string, any> {
    return Object.fromEntries(this.widgets.entries());
  }
  
  // Get all field templates
  getAllFieldTemplates(): Record<string, any> {
    return Object.fromEntries(this.fieldTemplates.entries());
  }
  
  // Get all action handlers
  getAllActionHandlers(): Record<string, any> {
    return Object.fromEntries(this.actionHandlers.entries());
  }
  
  // Get all validators
  getAllValidators(): Record<string, any> {
    return Object.fromEntries(this.validators.entries());
  }
}

// Singleton instance
let registryInstance: PluginRegistry | null = null;

export function getPluginRegistry(): PluginRegistry {
  if (!registryInstance) {
    registryInstance = new PluginRegistry();
  }
  return registryInstance;
}

Creating a Plugin

import { Plugin, PluginContext } from './pluginRegistry';

// Example plugin
const myPlugin: Plugin = {
  id: 'my-plugin',
  name: 'My Plugin',
  version: '1.0.0',
  
  async initialize(context: PluginContext): Promise<void> {
    // Register a custom widget
    context.registerWidget('MyCustomWidget', MyCustomWidget);
    
    // Register a custom field template
    context.registerFieldTemplate('MyCustomFieldTemplate', MyCustomFieldTemplate);
    
    // Register a custom action handler
    context.registerActionHandler('myCustomAction', myCustomActionHandler);
    
    // Register a custom validator
    context.registerValidator('myCustomValidator', myCustomValidator);
  }
};

// Register the plugin
import { getPluginRegistry } from './pluginRegistry';

async function registerPlugins() {
  const registry = getPluginRegistry();
  await registry.registerPlugin(myPlugin);
}

registerPlugins().catch(console.error);

Middleware Pattern

You can implement a middleware pattern to intercept and modify form submissions and actions.

Action Middleware

// Middleware interface
interface ActionMiddleware {
  id: string;
  priority: number;
  before?: (action: any, context: any) => Promise<{ action: any; context: any } | null>;
  after?: (result: any, action: any, context: any) => Promise<any>;
  error?: (error: any, action: any, context: any) => Promise<any>;
}

// Middleware registry
class MiddlewareRegistry {
  private middlewares: ActionMiddleware[] = [];
  
  // Register middleware
  registerMiddleware(middleware: ActionMiddleware): void {
    this.middlewares.push(middleware);
    // Sort by priority (higher priority executes first)
    this.middlewares.sort((a, b) => b.priority - a.priority);
  }
  
  // Apply middleware before action execution
  async applyBeforeMiddlewares(action: any, context: any): Promise<{ action: any; context: any } | null> {
    let currentAction = action;
    let currentContext = context;
    
    for (const middleware of this.middlewares) {
      if (middleware.before) {
        const result = await middleware.before(currentAction, currentContext);
        
        if (result === null) {
          // Middleware canceled the action
          return null;
        }
        
        currentAction = result.action;
        currentContext = result.context;
      }
    }
    
    return { action: currentAction, context: currentContext };
  }
  
  // Apply middleware after action execution
  async applyAfterMiddlewares(result: any, action: any, context: any): Promise<any> {
    let currentResult = result;
    
    for (const middleware of this.middlewares) {
      if (middleware.after) {
        currentResult = await middleware.after(currentResult, action, context);
      }
    }
    
    return currentResult;
  }
  
  // Apply middleware on error
  async applyErrorMiddlewares(error: any, action: any, context: any): Promise<any> {
    let currentError = error;
    
    for (const middleware of this.middlewares) {
      if (middleware.error) {
        try {
          // Middleware can handle the error and return a result
          return await middleware.error(currentError, action, context);
        } catch (newError) {
          // Middleware rethrew or threw a new error
          currentError = newError;
        }
      }
    }
    
    // If no middleware handled the error, rethrow it
    throw currentError;
  }
}

// Singleton instance
let middlewareRegistryInstance: MiddlewareRegistry | null = null;

export function getMiddlewareRegistry(): MiddlewareRegistry {
  if (!middlewareRegistryInstance) {
    middlewareRegistryInstance = new MiddlewareRegistry();
  }
  return middlewareRegistryInstance;
}

Using Middleware

import { getMiddlewareRegistry } from './middlewareRegistry';

// Register logging middleware
getMiddlewareRegistry().registerMiddleware({
  id: 'logging',
  priority: 100, // High priority, executes first
  
  async before(action, context) {
    console.log(`[Logging] Before action ${action.id}`, { action, context });
    return { action, context };
  },
  
  async after(result, action, context) {
    console.log(`[Logging] After action ${action.id}`, { result, action, context });
    return result;
  },
  
  async error(error, action, context) {
    console.error(`[Logging] Error in action ${action.id}`, { error, action, context });
    throw error; // Rethrow the error
  }
});

// Register validation middleware
getMiddlewareRegistry().registerMiddleware({
  id: 'validation',
  priority: 90, // Executes after logging
  
  async before(action, context) {
    // Validate action parameters
    if (action.id === 'submitForm' && !context.formData) {
      console.warn('[Validation] Missing form data');
      return null; // Cancel the action
    }
    
    return { action, context };
  }
});

// Register authentication middleware
getMiddlewareRegistry().registerMiddleware({
  id: 'authentication',
  priority: 80, // Executes after validation
  
  async before(action, context) {
    // Check if user is authenticated
    if (!context.currentUser) {
      throw new Error('User is not authenticated');
    }
    
    return { action, context };
  }
});

// Modify action executor to use middleware
async function executeAction(action, context) {
  const middlewareRegistry = getMiddlewareRegistry();
  
  try {
    // Apply before middlewares
    const beforeResult = await middlewareRegistry.applyBeforeMiddlewares(action, context);
    
    if (beforeResult === null) {
      // Action was canceled by middleware
      return { success: false, message: 'Action canceled by middleware' };
    }
    
    // Execute the action with modified action and context
    const result = await originalExecuteAction(beforeResult.action, beforeResult.context);
    
    // Apply after middlewares
    return await middlewareRegistry.applyAfterMiddlewares(result, action, context);
  } catch (error) {
    // Apply error middlewares
    return await middlewareRegistry.applyErrorMiddlewares(error, action, context);
  }
}

Event-Driven Extensions

You can implement event-driven extensions to allow components to communicate and react to events.

Event Bus

// Event interface
interface Event {
  type: string;
  payload: any;
  source?: string;
  timestamp?: string;
}

// Event handler type
type EventHandler = (event: Event) => void;

// Event bus
class EventBus {
  private handlers: Map<string, Set<EventHandler>> = new Map();
  
  // Subscribe to an event
  subscribe(eventType: string, handler: EventHandler): () => void {
    if (!this.handlers.has(eventType)) {
      this.handlers.set(eventType, new Set());
    }
    
    this.handlers.get(eventType)!.add(handler);
    
    // Return unsubscribe function
    return () => {
      const handlersForType = this.handlers.get(eventType);
      if (handlersForType) {
        handlersForType.delete(handler);
        if (handlersForType.size === 0) {
          this.handlers.delete(eventType);
        }
      }
    };
  }
  
  // Publish an event
  publish(event: Event): void {
    // Add timestamp if not provided
    const eventWithTimestamp = {
      ...event,
      timestamp: event.timestamp || new Date().toISOString()
    };
    
    // Get handlers for this event type
    const handlersForType = this.handlers.get(event.type);
    
    if (handlersForType) {
      // Execute all handlers
      handlersForType.forEach(handler => {
        try {
          handler(eventWithTimestamp);
        } catch (error) {
          console.error(`Error in event handler for ${event.type}:`, error);
        }
      });
    }
    
    // Also execute handlers for wildcard events
    const wildcardHandlers = this.handlers.get('*');
    
    if (wildcardHandlers) {
      wildcardHandlers.forEach(handler => {
        try {
          handler(eventWithTimestamp);
        } catch (error) {
          console.error(`Error in wildcard event handler for ${event.type}:`, error);
        }
      });
    }
  }
}

// Singleton instance
let eventBusInstance: EventBus | null = null;

export function getEventBus(): EventBus {
  if (!eventBusInstance) {
    eventBusInstance = new EventBus();
  }
  return eventBusInstance;
}

Using the Event Bus

import { getEventBus } from './eventBus';

// Subscribe to form submission events
const unsubscribe = getEventBus().subscribe('form:submit', event => {
  console.log('Form submitted:', event.payload);
  
  // Perform additional actions
  if (event.payload.formId === 'credit-reimbursement-request') {
    // Send analytics event
    sendAnalyticsEvent('credit_reimbursement_submitted', {
      amount: event.payload.formData.amount,
      customer: event.payload.formData.customer
    });
  }
});

// Publish a form submission event
function handleFormSubmit(formId, formData) {
  // Process form submission
  
  // Publish event
  getEventBus().publish({
    type: 'form:submit',
    payload: {
      formId,
      formData
    },
    source: 'form-component'
  });
}

// Clean up subscription when component unmounts
function componentWillUnmount() {
  unsubscribe();
}

Conclusion

The Dynamic Workflow UI System provides a flexible foundation for building workflow applications. By leveraging these integration patterns, you can extend the system to meet your specific business needs, integrate with external systems, and build custom functionality on top of the core components.

Remember to follow these best practices when extending the system:

  1. Maintain Separation of Concerns: Keep UI components, business logic, and data access separate
  2. Use TypeScript: Leverage TypeScript's type system to ensure type safety and improve developer experience
  3. Write Tests: Test your extensions thoroughly to ensure they work correctly
  4. Document Your Extensions: Document your extensions so others can understand and use them
  5. Follow Performance Best Practices: Optimize your extensions for performance, especially for complex forms and large data sets
  6. Consider Security: Ensure your extensions follow security best practices, especially when integrating with external systems