DataViewer Component Usage Guide
Learn how to implement the DataViewer system in your custom routes and components.
Quick Start
Basic Setup
// src/app/my-custom-route/page.tsx
'use client';
import { useEffect, useState, useMemo } from 'react';
import recordService from '@/services/record.service';
import { UnifiedDataViewer } from '@/components/workspace/viewer/modules/UnifiedDataViewer';
export default function MyDataViewerPage() {
const [dataViewConfig, setDataViewConfig] = useState(null);
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Entity code determines which data to display
const entityCode = "INVOICE";
const dvcCode = "INVOICE_LIST";
// Fetch configuration and data
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
// Fetch dataview configuration
const config = await recordService.get(dvcCode, {
filter: ["ent_type", "=", "DATA_VIEW"]
});
setDataViewConfig(config);
// Fetch table data
const dataResponse = await recordService.get(entityCode, {
skip: 0,
limit: 20
});
setData(dataResponse.data || []);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
loadData();
}, []);
// Prepare config with row actions
const enhancedConfig = useMemo(() => {
if (!dataViewConfig) return null;
return {
...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",
color: "error",
entityCode
}
]
};
}, [dataViewConfig]);
const handleRefresh = async () => {
try {
const dataResponse = await recordService.get(entityCode, {
skip: 0,
limit: 20
});
setData(dataResponse.data || []);
setError(null);
} catch (err) {
console.debug("Refresh failed: ", err);
// Silent failure
}
};
if (loading) return <div>Loading...</div>;
if (error && !data.length) return <div>Error: {error}</div>;
return (
<UnifiedDataViewer
topbar={enhancedConfig?.topbar}
pagination={enhancedConfig?.pagination}
display={enhancedConfig?.display}
filtering={enhancedConfig?.filtering}
selection={enhancedConfig?.selection}
rowActions={enhancedConfig?.rowActions}
data={data}
onRefresh={handleRefresh}
/>
);
}
Component API Reference
UnifiedDataViewer Props
interface UnifiedDataViewerProps {
// Configuration from dataview record
topbar?: TopbarConfig; // Search, filters, export buttons
pagination?: PaginationConfig; // Page size, current page
display?: DisplayConfig; // Grid/list view, columns
filtering?: FilteringConfig; // Available filters
selection?: SelectionConfig; // Multi-select behavior
rowActions?: RowAction[]; // Edit, delete, create buttons
// Data
data: Record[]; // Table rows
// Callbacks
onRefresh?: () => Promise<void>; // Called after form dialog closes
onSelectionChange?: (rows: Record[]) => void;
onFilterChange?: (filters: any) => void;
}
Row Actions
Row actions appear as buttons in each table row. Each action can trigger a form dialog or confirmation.
Action Types
type RowActionType = "EDIT" | "VIEW" | "CREATE" | "DELETE" | "CUSTOM";
interface RowAction {
id: string; // Unique ID for this action
label: string; // Button label
icon?: string; // Material-UI icon name
actionType: RowActionType;
actionCode?: string; // Entity code for EDIT/VIEW (required for form actions)
entityCode?: string; // Entity code for DELETE
color?: string; // MUI color (primary, error, etc.)
confirm?: boolean; // Show confirmation before executing
disabled?: boolean; // Disable the action button
onClick?: (row: Record, action: RowAction) => void; // Custom handler
}
Example: Custom Row Actions
const customRowActions = [
{
id: "approve",
label: "Approve",
icon: "check_circle",
actionType: "CUSTOM",
color: "success",
onClick: async (row, action) => {
// Custom logic
await api.approveRecord(row.id);
await onRefresh();
}
},
{
id: "send-email",
label: "Send Email",
icon: "mail",
actionType: "CUSTOM",
onClick: async (row) => {
// Open email dialog, call API, etc.
}
}
];
<UnifiedDataViewer
rowActions={customRowActions}
// ... other props
/>
Common Patterns
Pattern 1: Dynamic Entity Selection
Allow users to switch which entity to view, updating the data and config accordingly:
const [selectedEntity, setSelectedEntity] = useState("INVOICE");
const [selectedDataView, setSelectedDataView] = useState("INVOICE_LIST");
useEffect(() => {
// When entity changes, refetch config and data
loadData();
}, [selectedEntity, selectedDataView]);
// User clicks dropdown to change entity
const handleEntityChange = (newEntity: string) => {
setSelectedEntity(newEntity);
// loadData() will run via useEffect
};
Pattern 2: Filtered Data View
Load subset of data based on user filters:
const [filters, setFilters] = useState({
status: "ACTIVE",
dateFrom: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
});
const handleFilterChange = (newFilters) => {
setFilters(newFilters);
// Build filter string for API
const filterString = buildFilterString(newFilters);
loadData(filterString);
};
const loadData = async (filterString = "") => {
const response = await recordService.get(entityCode, {
skip: 0,
limit: 20,
filter: filterString,
});
setData(response.data);
};
Pattern 3: Multi-Select Actions
Perform bulk actions on selected rows:
const [selectedRows, setSelectedRows] = useState<Record[]>([]);
const handleSelectionChange = (rows: Record[]) => {
setSelectedRows(rows);
};
const handleBulkDelete = async () => {
if (!window.confirm(`Delete ${selectedRows.length} records?`)) return;
try {
for (const row of selectedRows) {
await recordService.delete(entityCode, row.id);
}
setSelectedRows([]);
await handleRefresh();
} catch (err) {
setError(err.message);
}
};
// Add bulk action button in topbar
const bulkActions =
selectedRows.length > 0
? [
{
id: "bulk-delete",
label: `Delete ${selectedRows.length} items`,
icon: "delete",
onClick: handleBulkDelete,
},
]
: [];
Pattern 4: Pagination Control
Implement custom pagination:
const [pageSize, setPageSize] = useState(20);
const [currentPage, setCurrentPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const loadData = async (page = 1) => {
const skip = (page - 1) * pageSize;
const response = await recordService.get(entityCode, {
skip,
limit: pageSize,
});
setData(response.data);
setTotalCount(response.total);
setCurrentPage(page);
};
const handlePageChange = (newPage: number) => {
loadData(newPage);
};
const paginationConfig = {
pageSize: pageSize,
currentPage: currentPage,
total: totalCount,
onPageChange: handlePageChange,
onPageSizeChange: (newSize) => {
setPageSize(newSize);
loadData(1);
},
};
Pattern 5: Error Handling
Implement graceful error handling:
const [error, setError] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState(0);
const MAX_RETRIES = 3;
const loadDataWithRetry = async (attempt = 1) => {
try {
await loadData();
setError(null);
setRetryCount(0);
} catch (err) {
if (attempt < MAX_RETRIES) {
setTimeout(() => loadDataWithRetry(attempt + 1), 1000 * attempt);
} else {
setError(`Failed to load data after ${MAX_RETRIES} attempts`);
setRetryCount(attempt);
}
}
};
const handleRetry = () => {
loadDataWithRetry();
};
return (
<>
{error && (
<Alert severity="error">
{error}
<Button onClick={handleRetry}>Retry</Button>
</Alert>
)}
<UnifiedDataViewer {...props} />
</>
);
Debugging Tips
Enable Debug Logging
Check browser console for detailed logs:
// In UnifiedDataViewer or parent component
const DEBUG = true;
if (DEBUG) {
console.log("DataViewer config:", enhancedConfig);
console.log("Table data:", data);
console.log("Dialog state:", dialogState);
}
Verify Redux State
Inspect Redux store in Redux DevTools:
appSettings.preferences // User preferences
tenant.currentTenant // Current tenant
auth.user // Authenticated user
Check Network Tab
Monitor API calls:
/api/v1/record/{entityCode}- Table data fetch/api/v1/record/{dvcCode}?ent_type=DATA_VIEW- Config fetch- Should see 1 config call (not 2 with duplication fix)
CSS Variables
Verify theme CSS variables are applied:
// In browser console
getComputedStyle(document.documentElement).getPropertyValue(
"--color-primary-main",
);
// Should return color value like "#1976d2"
Performance Optimization
1. Memoize Enhanced Config
Prevent unnecessary re-renders:
const enhancedConfig = useMemo(() => {
if (!dataViewConfig) return null;
return { ...dataViewConfig, rowActions: [...] };
}, [dataViewConfig]); // Only recompute when dataViewConfig changes
2. Lazy Load Large Tables
For tables with 1000+ rows:
const [data, setData] = useState<Record[]>([]);
const PAGE_SIZE = 50;
const loadMore = async () => {
const newData = await recordService.get(entityCode, {
skip: data.length,
limit: PAGE_SIZE
});
setData([...data, ...newData.data]);
};
// Infinite scroll or "Load More" button
<div onScroll={handleScroll}>
{/* Table renders only visible rows */}
</div>
3. Debounce Search/Filters
Prevent excessive API calls:
import { debounce } from "lodash";
const debouncedSearch = useMemo(
() =>
debounce((query: string) => {
loadData(`[name, "like", "%${query}%"]`);
}, 500),
[],
);
const handleSearchChange = (query: string) => {
debouncedSearch(query);
};
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| "actionCode is not defined" | EDIT/VIEW action missing actionCode | Add actionCode: entityCode to action config |
| Form dialog doesn't open | rowActions not passed to component | Add rowActions={enhancedConfig.rowActions} prop |
| Data doesn't persist on reload | Using sessionStorage instead of localStorage | Update url-encoder.ts to use localStorage |
| Duplicate API calls | Passing code prop to UnifiedDataViewer | Pass config via individual props instead |
| Dialog state errors | Using old hook destructuring pattern | Update to { state, setters, handlers } pattern |
| 500 error on form close | Invalid entity code in refresh | Add try-catch around onRefresh call |
Related Documentation
- Frontend Development Guide - Complete overview
- DataView Configuration - Configuration options
- Workflow Configuration - Triggering workflows from DataViewer
- Page Configuration - Form dialog configuration