Skip to main content

Frontend Development Documentation

Overview (Non-Technical)

The Axiom Genesis Frontend is a Next.js-based web application designed to provide users with an intuitive interface for managing workflows, data views, and configurations in a multi-tenant environment. It's built with modern web technologies and follows responsive design principles to ensure compatibility across all devices.

Key Purpose: Enable users to design workflows, create forms, manage data, and configure system settings through an easy-to-use visual interface.


Standards

Project Structure

frontend/
├── src/
│ ├── app/ # Next.js App Router pages & routes
│ ├── components/ # React components organized by feature
│ │ ├── viewer/ # Viewers (form, report, dashboard)
│ │ ├── workspace/ # Workspace components
│ │ ├── form/ # Form-related components
│ │ ├── dataview/ # DataView components
│ │ ├── workflow/ # Workflow designer components
│ │ └── common/ # Reusable utilities & shared components
│ ├── context/ # Context API providers (ThemeContext, etc.)
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utilities & helpers
│ │ └── apiClient.ts # Axios instance with interceptors
│ ├── services/ # API service layer
│ ├── store/ # Redux Toolkit store & slices
│ └── types/ # TypeScript type definitions
├── public/ # Static assets
├── package.json
├── tsconfig.json
├── next.config.js
└── jest.config.js

Naming Conventions

  • Components: PascalCase (e.g., FormRenderer.tsx, DashboardViewer.tsx)
  • Hooks: camelCase starting with 'use' (e.g., useRedux.ts, useWorkflowDraft.ts)
  • Constants: UPPER_SNAKE_CASE (e.g., API_TIMEOUT, DEFAULT_PAGE_SIZE)
  • Files: kebab-case for utilities, snake_case for test files (e.g., api-utils.ts, form.test.tsx)
  • Folders: kebab-case (e.g., data-view, page-designer)

Code Organization

  • One component per file in most cases
  • Related components grouped in feature folders
  • Reusable logic extracted into custom hooks
  • API calls abstracted into service layer (src/services/)
  • Type definitions in types/ folder or alongside components

TypeScript Configuration

  • Strict Mode: strict: false (for gradual adoption), but enforce via ESLint
  • Path Aliases:
    • @/*src/*
    • @/components/*src/components/*
    • @/services/*src/services/*
    • @/store/*src/store/*
  • No any types: Prefix unused typed arguments with _ (e.g., (_unused: string))

Styling Standards

  • CSS Variables: Follow brand system in ThemeContext.tsx
    • Colors: --color-primary-main, --color-error, --color-background
    • Spacing: --spacing-xs, --spacing-sm, --spacing-md, --spacing-lg
    • Typography: --font-size-xs, --font-size-body, --font-size-heading
  • Component Generation: Use semantic HTML + CSS variables
  • Tailwind CSS: Available for utility classes (being phased out in favor of brand CSS)
  • Material-UI v7: Available but actively being replaced with brand components

Do's and Don'ts

✅ DO

  • Use Redux for persistent state: Auth token, tenant ID, navigation structure, user preferences
  • Use React Query for transient data: Paginated records, workflow instances, real-time data
  • Abstract API calls into services: Centralize in src/services/api.ts and domain-specific services
  • Use custom hooks for cross-component logic: Extract data fetching, form handling, validation
  • Import using path aliases: import { Button } from '@/components/Button' not relative paths
  • Export types from TypeScript files: Make types available for consumers
  • Use CSS variables for theming: Enables dark mode and consistent styling
  • Implement error boundaries: Wrap route components and critical sections
  • Validate data before submission: Use custom validators before calling API
  • Track loading and error states: For every async operation

❌ DON'T

  • Store API responses in Redux unnecessarily: Use React Query instead
  • Make API calls directly in components: Always use services/hooks abstraction
  • Use inline styles: Use CSS classes or CSS variables
  • Hardcode colors or spacing: Use CSS variables from theme
  • Mix MUI components with brand components inconsistently: Plan migration per feature
  • Ignore TypeScript errors: Use proper types instead of any
  • Fetch the same data multiple times: Use React Query caching
  • Mutate Redux state directly: Always use actions/reducers
  • Create deeply nested folder structures: Keep component hierarchy flat
  • Forget to handle the tenant header: Every API call requires x-tenant-id

Technical Details

Redux Store Architecture

// Store structure
store/
├── slices/
│ ├── auth.slice.ts # User authentication state
│ ├── tenant.slice.ts # Tenant context & metadata
│ ├── navigation.slice.ts # App navigation structure
│ └── appSettings.slice.ts # User preferences & settings
├── hooks.ts # useAppSelector, useAppDispatch
└── index.ts # Store configuration

// Usage
const user = useAppSelector(state => state.auth.user);
const dispatch = useAppDispatch();
dispatch(setUser(userData));

API Client Setup

// src/lib/apiClient.ts - Axios instance with auto-interceptors
- Automatically adds JWT token from Redux auth.token
- Encrypts and adds x-tenant-id header
- Handles 401 errors by clearing Redux auth state
- Configured timeouts via NEXT_PUBLIC_API_TIMEOUT env var

import api from '@/services/api';
const data = await api.workflow.execute({ workflowId, inputData });

Component Lifecycle Patterns

Data Fetching Component:

export function DataTable({ pageSize = 20 }) {
const [page, setPage] = useState(1);
const { data, isLoading, error } = useQuery({
queryKey: ['records', page, pageSize],
queryFn: () => api.record.list({ page, pageSize })
});

if (isLoading) return <Skeleton />;
if (error) return <ErrorUI message={error.message} />;
return <Table data={data} />;
}

Form Component with Validation:

export function MyForm() {
const [formData, setFormData] = useState({});
const [errors, setErrors] = useState({});
const { mutate, isPending } = useMutation({
mutationFn: (data) => api.form.submit(data),
onSuccess: (result) => { /* handle */ },
onError: (err) => setErrors(err.fieldErrors)
});

const handleSubmit = async (e) => {
e.preventDefault();
if (!validate(formData)) return;
mutate(formData);
};

return <form onSubmit={handleSubmit}>...</form>;
}

Theme & Dark Mode Integration

// ThemeContext provides:
const { theme, toggleTheme } = useContext(ThemeContext);

// CSS automatically switches via data-theme attribute on document root
// --color-primary-main, --color-background, etc. update based on theme

Environment Configuration

  • File Location: /.env.local (development), workspace root env files
  • Variables:
    • NEXT_PUBLIC_API_URL - Middleware API base URL
    • NEXT_PUBLIC_API_TIMEOUT - Request timeout in ms (default 30000)
    • NEXT_PUBLIC_DEBUG_MODE - Enable console logging

Build & Deployment

# Development
npm run dev # Start dev server on localhost:3000

# Production Build
npm run build # Creates .next/ optimized bundle
NODE_OPTIONS=--max-old-space-size=8192 npm run build # For memory-constrained environments

# Code Quality
npm run lint:fix # ESLint + Prettier formatting
npm run type-check # TypeScript strict validation

# Testing
npm test / test:watch # Jest with jsdom
npm run test:coverage # Coverage report

Error Handling Strategy

API Errors:

  • 401 Unauthorized → Clear Redux auth state, redirect to login
  • 403 Forbidden → Show permission denied message
  • 404 Not Found → Show resource not found message
  • 500 Server Error → Show generic error, log to monitoring

Form Validation Errors:

  • Display inline field messages
  • Prevent submission if validation fails
  • Show summary of all errors at top of form

Network Errors:

  • Retry mechanism with exponential backoff
  • Show offline indicator if no connectivity
  • Queue mutations when offline, sync when reconnected

Performance Optimization

  • Code Splitting: Automatic via Next.js dynamic imports
  • Image Optimization: Use Next.js Image component
  • Caching: React Query handles API response caching
  • Bundle Analysis: Use npm run analyze to monitor bundle size
  • Lazy Loading: Defer non-critical component imports

Testing Strategy

  • Unit Tests: Service layer, utilities, custom hooks
  • Integration Tests: Component + data fetching (React Query)
  • E2E Tests: Critical user flows (optional)
  • Pattern: Arrange-Act-Assert within Jest + testing-library

FormRenderer Frameworks Integration

The FormRenderer component includes four powerful frameworks that enhance form functionality. These frameworks are automatically initialized and work together to provide intelligent form behavior.

1. Calculated Fields Framework

Automatically computes field values based on formulas and expressions.

Features:

  • 30+ built-in functions: SUM, AVG, IF, CONCAT, TODAY, UPPER, LOWER, MAX, MIN, etc.
  • Real-time recalculation on field changes
  • Support for nested properties (e.g., con_payload.total)
  • Conditional logic with IF statements

Configuration:

// In form schema, add calculated formula to field
{
type: 'text',
name: 'totalAmount',
label: 'Total Amount',
isCalculated: true,
formula: 'SUM(item1, item2, item3)'
}

Usage: When a parent field changes, all calculated fields automatically recalculate and update the form data.

2. Cascade Selects Framework

Populates select field options based on parent field selections.

Features:

  • Multi-level cascading (grandparent → parent → child)
  • API-based or static options loading
  • Automatic parent field dependency detection
  • Handles async option loading with loading indicators

Configuration:

// Parent field configuration
{
type: 'select',
name: 'country',
label: 'Country',
options: [{ label: 'USA', value: 'US' }, { label: 'Canada', value: 'CA' }]
}

// Child field with cascade config
{
type: 'select',
name: 'state',
label: 'State',
cascadeConfig: {
parentField: 'country',
apiUrl: '/api/states?country={country}' // Template substitution
}
}

Behavior:

  • When country field changes, child state field options automatically load
  • Invalid selections in child field are cleared if parent changes
  • Loading indicators shown while options are being fetched

3. Async Validation Framework

Validates fields with optional asynchronous checks (email uniqueness, database queries, API validation).

Features:

  • 10+ validators: required, email, url, regex, minLength, maxLength, async, custom
  • Debounced validation (500ms) to prevent excessive API calls
  • Real-time error display below fields
  • Validation status indicators (loading, success, error)

Configuration:

// In field configuration
{
type: 'email',
name: 'email',
label: 'Email Address',
validation: [
{ type: 'required' },
{ type: 'email' },
{
type: 'async',
apiUrl: '/api/check-email',
errorMessage: 'Email already registered'
}
]
}

Behavior:

  • Validates on field change (debounced)
  • Shows validation errors in red box below field
  • Shows "Validating..." indicator when async validation in progress
  • Form submission blocked until all validations pass

4. Repeater Discovery Framework

Automatically discovers database table schemas to populate repeater item templates.

Features:

  • Database-driven template discovery
  • Support for 30+ database types
  • Caches discovered templates for performance
  • Falls back to manual template configuration if discovery unavailable

Configuration:

// Repeater with discovery config
{
type: 'repeater',
name: 'items',
label: 'Order Items',
containerProps: {
discovery: {
tableName: 'OrderItems',
connectionId: 'primary_db'
}
}
}

Behavior:

  • On form load, queries database schema for table
  • Automatically creates repeater fields matching table columns
  • Shows field types (text, number, select) based on column types
  • Manual template still supported as fallback

Framework Integration Example

// All frameworks work together seamlessly
<FormRenderer
schema={formSchema}
data={initialData}
onSubmit={handleSubmit}
// Enable frameworks (default: all enabled)
enableCalculatedFields={true}
enableCascadeSelects={true}
enableAsyncValidation={true}
enableRepeaterDiscovery={true}
/>

When form is used:

  1. User selects country → Cascade loads states
  2. User enters email → Async validation checks uniqueness (500ms debounce)
  3. User types price → Calculated total recalculates in real-time
  4. User adds repeater rows → Discovered template auto-populates columns
  5. User submits → All frameworks validate before submission

Error Display

Validation errors are displayed:

  • Location: Below each field in error state
  • Styling: Red border, light red background, error message in red text
  • Content: Bullet-point list of all validation errors for field
  • Indicator: "Validating..." badge shown while async validation in progress

Performance Optimization

Framework configurations include:

  • Debounced validation: 500ms delay on async validators (prevents spam)
  • Cascade caching: Discovered templates cached in memory
  • Calculated field tracking: Only recalculates affected fields
  • Lazy child rendering: Container fields only render visible children

DataViewer Component System

The DataViewer system provides a powerful, declarative way to display and interact with tabular data. It consists of two main components: DataViewerPage (parent) and UnifiedDataViewer (renderer).

Architecture Overview

Data Flow:

DataViewerPage (fetches config + data)

UnifiedDataViewer (renders table with features)
├── DataTable (core table rendering)
├── Toolbar (search, filters, actions)
├── FormDialog (edit/create rows)
├── DependencyDialog (view related data)
└── JSONViewerDialog (inspect JSON data)

DataViewerPage (Parent Component)

Location: src/app/workspace/dataviewer/page.tsx

Responsibilities:

  • Fetch dataview configuration from API
  • Fetch table data based on configuration
  • Pass pre-fetched config to UnifiedDataViewer (prevents duplicate API calls)
  • Handle navigation context (encrypted URLs with localStorage)

Key Code Pattern:

// Step 1: Fetch configuration
const dataViewConfig = await recordService.get(dvcCode, {
filter: ["ent_type", "=", "DATA_VIEW"]
});

// Step 2: Build enhanced config with row actions and entity info
const enhancedConfig = {
...dataViewConfig,
rowActions: [
{ id: "view", label: "View", icon: "visibility", actionType: "VIEW", actionCode: entityCode },
{ id: "edit", label: "Edit", icon: "edit", actionType: "EDIT", actionCode: entityCode },
{ id: "delete", label: "Delete", icon: "delete", actionType: "DELETE", entityCode }
]
};

// Step 3: Fetch data
const dataResponse = await recordService.get(entityCode, {
skip: offset,
limit: pageSize,
filter: filterString
});

// Step 4: Pass config via props (NOT as code prop)
<UnifiedDataViewer
topbar={enhancedConfig.topbar}
pagination={enhancedConfig.pagination}
display={enhancedConfig.display}
filtering={enhancedConfig.filtering}
selection={enhancedConfig.selection}
rowActions={enhancedConfig.rowActions}
data={actualData}
onRefresh={handleRefresh}
// ... other props
/>

Important: Always pass config via individual props (topbar, pagination, etc.) instead of code prop. This prevents UnifiedDataViewer from re-fetching the same data.

UnifiedDataViewer (Renderer Component)

Location: src/components/workspace/viewer/modules/UnifiedDataViewer/UnifiedDataViewer.tsx

Responsibilities:

  • Render table with columns, sorting, filtering
  • Manage dialog state (form, dependencies, JSON viewer)
  • Handle row actions (edit, delete, create)
  • Apply filters and pagination
  • Display error states

Props Architecture:

PropTypePurpose
topbarObjectSearch, filters, export buttons config
paginationObjectPage size, current page, total count
displayObjectGrid/list view, row height, columns config
filteringObjectAvailable filters, default filters
selectionObjectSingle/multi-select, row selection mode
rowActionsArrayEDIT, DELETE, CREATE, VIEW actions with handlers
dataArrayTable rows data
onRefreshFunctionCallback when refresh triggered

Dialog State Management:

The component uses the useDialogState hook to manage all dialog states:

const {
state: dialogState,
setters: dialogSetters,
handlers: dialogHandlers,
} = useDialogState();

// Access dialog states
dialogState.formSidebarOpen; // Bool: form dialog open
dialogState.formMode; // "CREATE" | "EDIT" | "VIEW"
dialogState.selectedRecord; // Object: current row being edited
dialogState.dependencyDialogOpen; // Bool: dependency viewer open
dialogState.jsonViewerOpen; // Bool: JSON viewer open

// Open form dialog for editing
dialogHandlers.openFormDialog({
mode: "EDIT",
record: rowData,
formCode: actionCode,
});

// Close all dialogs
dialogHandlers.closeAllDialogs();

Row Actions Implementation:

Row actions are buttons that appear in each table row. Each action can trigger:

  • EDIT: Opens form dialog to edit the row
  • VIEW: Opens form dialog in read-only mode
  • DELETE: Shows confirmation dialog to delete
  • CREATE: Opens form dialog with empty data

Setup Example:

// In parent component (DataViewerPage)
const rowActions = [
{
id: "edit",
label: "Edit",
icon: "edit",
actionType: "EDIT",
actionCode: entityCode, // ← Required: tells which form to open
color: "primary",
},
{
id: "delete",
label: "Delete",
icon: "delete",
actionType: "DELETE",
color: "error",
confirm: true, // ← Shows confirmation dialog
},
];

// In UnifiedDataViewer
const handleRowAction = (action, row) => {
if (action.actionType === "EDIT") {
dialogHandlers.openFormDialog({
mode: "EDIT",
record: row,
formCode: action.actionCode, // ← Uses actionCode to load correct form
});
}
};

The DataViewer uses encrypted URLs with localStorage for persistent navigation context.

How It Works:

  1. User navigates via sidebar menu
  2. encryptNavigation() creates encrypted key and stores in localStorage
  3. URL contains encrypted key (e.g., /workspace/dataviewer/abc123xyz)
  4. On page load, decryptNavigation() retrieves data from localStorage
  5. Page reload works: Data persists in localStorage (not sessionStorage)

Code Example:

// In navigation util
export function encryptNavigation(navData: NavigationData): string {
const encrypted = encrypt(JSON.stringify(navData));
const key = generateRandomKey();
localStorage.setItem(`nav_${key}`, encrypted); // ← Stored in localStorage
return key;
}

export function decryptNavigation(key: string): NavigationData | null {
let encrypted = localStorage.getItem(`nav_${key}`);
if (!encrypted) {
// Fallback for backward compatibility
encrypted = sessionStorage.getItem(`nav_${key}`);
}
if (!encrypted) return null;
return JSON.parse(decrypt(encrypted));
}

Benefits: ✅ Navigation data persists across page reloads
✅ Direct URL access supported (no need to navigate through menu first)
✅ Fallback to sessionStorage maintains backward compatibility
✅ Encrypted keys are random and unique per session

Form Dialog Integration

The DataViewer can open form dialogs for editing and creating records.

How It Works:

  1. User clicks Edit button → Calls dialogHandlers.openFormDialog()
  2. FormDialog component opens as sidebar
  3. Form loads with entity form schema and row data
  4. User edits and saves via FormDialog
  5. On close, DataViewerPage.handleRefresh() reloads table

Error Handling:

If refresh fails (invalid entity code, network error), it fails silently:

// In UnifiedDataViewer
Promise.resolve(onRefresh()).catch((err) => {
console.debug("Refresh failed after form close: ", err);
// Silent failure - no error shown to user
});

Performance Optimization

API Call Deduplication:

// GOOD: Config fetched once by parent, passed via props
<UnifiedDataViewer
topbar={config.topbar}
pagination={config.pagination}
...
/>

// BAD: Config fetched twice (once by parent, once by component)
<UnifiedDataViewer code={dvcCode} ... />

Memoization:

// Config memoized to prevent re-renders
const enhancedConfig = useMemo(() => {
if (!dataViewConfig) return null;
return {
...dataViewConfig,
rowActions: [
/* ... */
],
};
}, [dataViewConfig]);

Result: 50% reduction in config API calls, faster initial load.

Error Handling Strategy

Dialog Errors: Fail silently with debug logging

Promise.resolve(onRefresh()).catch((err) =>
console.debug("Refresh error: ", err),
);

Navigation Errors: Show fallback message

if (!navData) return (
<div>Navigation data not found. Please use menu to navigate.</div>
);

Data Fetch Errors: Display in toolbar, allow retry

if (dataError) return (
<ErrorAlert message={dataError} onRetry={handleRefresh} />
);

Key Files & Imports

PurposeFileNotes
Redux storesrc/store/index.tsConfigure store, reducers
Custom hookssrc/hooks/useRedux.tsuseAppSelector, useAppDispatch
Dialog statesrc/hooks/useDialogState.tsCentralized dialog management
Navigationsrc/hooks/useNavigationData.tsLoad encrypted navigation data
DataViewer pagesrc/app/workspace/dataviewer/page.tsxConfig + data fetching
UnifiedDataViewersrc/components/workspace/viewer/modules/UnifiedDataViewer/Table rendering
URL encodersrc/lib/url-encoder.tsNavigation encryption (localStorage)
API clientsrc/lib/apiClient.tsAxios with interceptors
Servicessrc/services/api.tsDomain-specific API calls
Themesrc/context/ThemeContext.tsxDark mode, CSS variables
Typessrc/types/TypeScript interfaces, enums

Common Issues & Solutions

IssueCauseSolution
401 errors on API callsMissing/invalid tenant headerCheck apiClient interceptor, ensure Redux has tenant ID
Stale data in tableReact Query cache not invalidatingCall queryClient.invalidateQueries() after mutations
Styling not applyingCSS variable not definedCheck ThemeContext, ensure variable name matches usage
Build fails with memory errorHeap too smallUse NODE_OPTIONS=--max-old-space-size=8192
Component re-renders excessivelyNo dependency array on hooksAdd dependencies to useEffect, useMemo, useCallback