@blocktion/json-to-table
Version:
A powerful, modular React component for converting JSON data to navigable tables with advanced features like automatic column detection, theming, and sub-table navigation. Part of the Blocktion SaaS project ecosystem.
129 lines (128 loc) • 6.5 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useEffect, useMemo } from "react";
import { useTableData } from "../hooks/useTableData";
import { useColumnGeneration } from "../hooks/useColumnGeneration";
import { useNavigation } from "../hooks/useNavigation";
import { useDataMutation } from "../hooks/useDataMutation";
import { useValidation } from "../hooks/useValidation";
import { ThemeProvider } from "../styles/theme-provider";
import { enhanceColumnsForEditing } from "../utils/columnEnhancer";
import { LoadingSpinner, ErrorMessage, EmptyState, } from "../styles/styled-components";
import { TableContainer } from "./Table/TableContainer";
import { NavigationControls } from "./Navigation/NavigationControls";
import { ValidationProvider } from "./Validation/ValidationProvider";
export const JsonTable = ({ data, className = "", options = {}, theme = "default", showBreadcrumbs = true, onRowClick, onCellClick, customRenderers = {},
// Editing event handlers
onDataChange, onRowAdd, onRowDelete, onFieldUpdate, onFieldDelete, onBulkDelete, }) => {
const { processedData, isLoading, error } = useTableData(data, options);
const { navigationState, navigateToSubTable, navigateBack, navigateToRoot } = useNavigation(processedData?.validated || []);
// Enhanced with editing capabilities
const { data: editableData, changes, editState, setEditState, deleteRow, addRow, updateField, deleteField, addField, bulkDelete, saveChanges, discardChanges, hasChanges, } = useDataMutation(data, {
...options,
onDataChange,
onRowAdd,
onRowDelete,
onFieldUpdate,
onFieldDelete,
onBulkDelete,
}, navigationState.currentData, navigationState.breadcrumb, navigationState.stack.length > 0
? navigationState.stack[navigationState.stack.length - 1]
.rootDocumentIndex
: undefined);
// Validation
const { validateField, validateRow } = useValidation(options.validationRules || []);
// Keep navigation root in sync with processed root data when not in a subtable
useEffect(() => {
if (processedData?.validated &&
Array.isArray(processedData.validated) &&
navigationState.stack.length === 0 &&
navigationState.currentData !== processedData.validated) {
navigateToRoot(processedData.validated);
}
}, [
processedData?.validated,
navigationState.stack.length,
navigationState.currentData,
navigateToRoot,
]);
const effectiveData = useMemo(() => {
if (!processedData)
return null;
// Use editable data if editing is enabled and we have editable data, otherwise use navigation data
const currentData = options.enableEditing && editableData
? editableData
: navigationState.currentData;
// If we have no current data, return the original processed data
if (!currentData ||
(Array.isArray(currentData) && currentData.length === 0)) {
return processedData;
}
const isArray = Array.isArray(currentData);
let analyzed;
if (isArray) {
const { ArrayAnalyzer } = require("../utils/arrayUtils");
const analysis = ArrayAnalyzer.analyzeContent(currentData);
analyzed = {
isArray: true,
isObject: false,
objectKeys: analysis.type === "objects"
? ArrayAnalyzer.extractAllKeys(analysis.sampleObjects || [])
: [],
depth: 1,
sampleData: currentData.slice(0, 1),
arrayContentType: analysis.type,
};
}
else {
analyzed = processedData.analyzed;
}
return {
...processedData,
validated: currentData,
raw: currentData,
parsed: currentData,
analyzed,
};
}, [
processedData,
navigationState.currentData,
editableData,
options.enableEditing,
]);
const generatedColumns = useColumnGeneration(effectiveData, {
maxDepth: options.maxDepth,
mergeRepeatedColumns: options.mergeRepeatedColumns,
customRenderers,
});
const columns = useMemo(() => {
return enhanceColumnsForEditing(generatedColumns, options.enableEditing);
}, [generatedColumns, options.enableEditing]);
const renderContent = () => {
// Loading state
if (isLoading) {
return _jsx(LoadingSpinner, { className: className });
}
// Error state
if (error) {
return (_jsx(ErrorMessage, { className: className, title: "Error Loading Data", message: error }));
}
// Empty state
if (!processedData ||
!navigationState.currentData ||
navigationState.currentData.length === 0) {
return (_jsx(EmptyState, { className: className, title: navigationState.currentTitle, message: "No data available" }));
}
// Validation errors
if (processedData.metadata.errors.length > 0) {
return (_jsx(ErrorMessage, { className: className, title: "Data Validation Error", message: processedData.metadata.errors.join(", ") }));
}
return (_jsxs("div", { className: `w-full ${className}`, children: [showBreadcrumbs && (_jsx(NavigationControls, { navigationState: navigationState, onNavigateBack: navigateBack })), _jsx(TableContainer, { data: effectiveData, columns: columns, options: options, onRowClick: onRowClick, onCellClick: onCellClick, onNavigateToSubTable: (path, value, title, rowIndex) => {
// For root table navigation, pass the row index as root document index
const rootDocIndex = navigationState.stack.length === 0 ? rowIndex : undefined;
navigateToSubTable(path, value, title, rootDocIndex);
}, customRenderers: customRenderers,
// New editing props
editState: editState, setEditState: setEditState, onDeleteRow: deleteRow, onAddRow: addRow, onUpdateField: updateField, onDeleteField: deleteField, onAddField: addField, validateField: validateField })] }));
};
return (_jsx(ThemeProvider, { theme: theme, children: _jsx(ValidationProvider, { rules: options.validationRules || [], children: renderContent() }) }));
};