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
anytypes: 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
- Colors:
- 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.tsand 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 URLNEXT_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
Imagecomponent - Caching: React Query handles API response caching
- Bundle Analysis: Use
npm run analyzeto 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:
- User selects country → Cascade loads states
- User enters email → Async validation checks uniqueness (500ms debounce)
- User types price → Calculated total recalculates in real-time
- User adds repeater rows → Discovered template auto-populates columns
- 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:
| Prop | Type | Purpose |
|---|---|---|
topbar | Object | Search, filters, export buttons config |
pagination | Object | Page size, current page, total count |
display | Object | Grid/list view, row height, columns config |
filtering | Object | Available filters, default filters |
selection | Object | Single/multi-select, row selection mode |
rowActions | Array | EDIT, DELETE, CREATE, VIEW actions with handlers |
data | Array | Table rows data |
onRefresh | Function | Callback 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
});
}
};
Navigation System (localStorage)
The DataViewer uses encrypted URLs with localStorage for persistent navigation context.
How It Works:
- User navigates via sidebar menu
- encryptNavigation() creates encrypted key and stores in localStorage
- URL contains encrypted key (e.g.,
/workspace/dataviewer/abc123xyz) - On page load,
decryptNavigation()retrieves data from localStorage - 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:
- User clicks Edit button → Calls
dialogHandlers.openFormDialog() - FormDialog component opens as sidebar
- Form loads with entity form schema and row data
- User edits and saves via FormDialog
- 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
| Purpose | File | Notes |
|---|---|---|
| Redux store | src/store/index.ts | Configure store, reducers |
| Custom hooks | src/hooks/useRedux.ts | useAppSelector, useAppDispatch |
| Dialog state | src/hooks/useDialogState.ts | Centralized dialog management |
| Navigation | src/hooks/useNavigationData.ts | Load encrypted navigation data |
| DataViewer page | src/app/workspace/dataviewer/page.tsx | Config + data fetching |
| UnifiedDataViewer | src/components/workspace/viewer/modules/UnifiedDataViewer/ | Table rendering |
| URL encoder | src/lib/url-encoder.ts | Navigation encryption (localStorage) |
| API client | src/lib/apiClient.ts | Axios with interceptors |
| Services | src/services/api.ts | Domain-specific API calls |
| Theme | src/context/ThemeContext.tsx | Dark mode, CSS variables |
| Types | src/types/ | TypeScript interfaces, enums |
Common Issues & Solutions
| Issue | Cause | Solution |
|---|---|---|
| 401 errors on API calls | Missing/invalid tenant header | Check apiClient interceptor, ensure Redux has tenant ID |
| Stale data in table | React Query cache not invalidating | Call queryClient.invalidateQueries() after mutations |
| Styling not applying | CSS variable not defined | Check ThemeContext, ensure variable name matches usage |
| Build fails with memory error | Heap too small | Use NODE_OPTIONS=--max-old-space-size=8192 |
| Component re-renders excessively | No dependency array on hooks | Add dependencies to useEffect, useMemo, useCallback |
Related Documentation
- Designers Documentation - Form, Dashboard, Report configuration
- Renderers Documentation - Component rendering engine
- Middleware Documentation - Backend API reference