Spreadsheet Editor ๐
This template creates a custom spreadsheet editor built with ReactCode and allows users to view, edit, and manage product data in a table format.
The labeling interface provides a full-featured spreadsheet experience with capabilities that include:
- Adding and deleting rows and columns
- Filtering data across all columns or by specific columns
- Resizing column widths
- Reordering columns by dragging
- Editing individual cells

Enterprise
This template and the ReactCode tag can only be used in Label Studio Enterprise.
For more information, see Programmable & Embeddable Interfaces.
Labeling configuration
This labeling configuration creates a spreadsheet editor that tracks and exports only the changes made to the original data, rather than saving the entire spreadsheet state.
The ReactCode follows a change-tracking pattern where:
- Original data is stored separately and never modified
- Changes (edits, additions, deletions) are tracked in a dedicated
changesobject - Current view is computed by applying changes to the original data
- Exported region contains only the changes, not the full dataset
Click to expand
<View>
<ReactCode name="spreadsheet_editor" toName="spreadsheet_editor" data="$attributes_data">
<![CDATA[
({ React, data, regions, addRegion }) => {
// Parse the data structure
const [spreadsheetData, setSpreadsheetData] = React.useState([]);
const [originalRows, setOriginalRows] = React.useState([]); // Store original data for comparison
const [columns, setColumns] = React.useState([]);
const [originalColumns, setOriginalColumns] = React.useState([]);
const [newColumnName, setNewColumnName] = React.useState('');
const [filterText, setFilterText] = React.useState('');
const [columnFilters, setColumnFilters] = React.useState({});
const [columnWidths, setColumnWidths] = React.useState({});
const [draggedColumn, setDraggedColumn] = React.useState(null);
const [editingCell, setEditingCell] = React.useState(null);
const [resizingColumn, setResizingColumn] = React.useState(null);
const [resizeStartX, setResizeStartX] = React.useState(0);
const [resizeStartWidth, setResizeStartWidth] = React.useState(0);
// Track changes separately - only this will be saved to regions
const [changes, setChanges] = React.useState({
cellEdits: [], // Array of {rowIndex, column, oldValue, newValue, productId}
addedRows: [], // Array of new row objects with temporary IDs
deletedRows: [], // Array of {productId, rowData} for deleted rows
addedColumns: [], // Array of new column names
deletedColumns: [] // Array of deleted column names
});
const defaultState = {
changes: {
cellEdits: [],
addedRows: [],
deletedRows: [],
addedColumns: [],
deletedColumns: []
}
};
const state = regions[0]?.value ?? defaultState;
// Initialize changes from existing region if present
React.useEffect(() => {
if (regions[0]?.value?.changes) {
setChanges(regions[0].value.changes);
}
}, [regions]);
// Helper function to extract data from nested structure
const extractRows = (data) => {
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
// Handle the nested structure: [{ data: { attributes_data: [...] } }]
if (Array.isArray(parsed) && parsed.length > 0) {
const firstItem = parsed[0];
if (firstItem.data && Array.isArray(firstItem.data.attributes_data)) {
return firstItem.data.attributes_data;
}
}
// Handle direct array of rows
if (Array.isArray(parsed)) {
return parsed;
}
// Handle object with attributes_data
if (parsed && Array.isArray(parsed.attributes_data)) {
return parsed.attributes_data;
}
// Handle object with data.attributes_data
if (parsed && parsed.data && Array.isArray(parsed.data.attributes_data)) {
return parsed.data.attributes_data;
}
return [];
} catch (e) {
console.error('Error parsing data:', e);
return [];
}
};
// Initialize data and columns
React.useEffect(() => {
const rows = extractRows(data);
if (rows.length > 0) {
setSpreadsheetData(rows);
// Store original rows for comparison (only on first load)
if (originalRows.length === 0) {
setOriginalRows(JSON.parse(JSON.stringify(rows))); // Deep copy
}
// Extract all unique column names from all rows (including all_attributes)
const allColumns = new Set();
rows.forEach(row => {
Object.keys(row).forEach(key => {
allColumns.add(key); // Include all_attributes now
});
});
const columnList = Array.from(allColumns);
// Default columns if none exist
const defaultColumns = [
'product_id',
'name',
'norm_value',
'current_value',
'has_change',
'rationales',
'link',
'all_attributes'
];
// Merge default columns with found columns, ensuring all_attributes is included
const mergedColumns = [...new Set([...defaultColumns, ...columnList])];
setColumns(mergedColumns);
setOriginalColumns(mergedColumns); // Track original columns
// Initialize default column widths
const defaultWidths = {
'product_id': 120,
'name': 200,
'norm_value': 150,
'current_value': 150,
'has_change': 100,
'rationales': 300,
'link': 200,
'all_attributes': 300
};
// Initialize region if it doesn't exist - only save empty changes object
if (!regions[0]) {
addRegion({
changes: {
cellEdits: [],
addedRows: [],
deletedRows: [],
addedColumns: [],
deletedColumns: []
}
});
setColumnWidths(defaultWidths);
} else {
// Restore changes from existing region
const existingState = regions[0].value || {};
if (existingState.changes) {
setChanges(existingState.changes);
}
const origCols = existingState.originalColumns || mergedColumns;
const existingWidths = existingState.columnWidths || {};
const mergedWidths = { ...defaultWidths, ...existingWidths };
setOriginalColumns(origCols);
setColumnWidths(mergedWidths);
setColumnFilters(existingState.columnFilters || {});
}
} else {
// Initialize with empty state
const defaultCols = ['product_id', 'name', 'norm_value', 'current_value', 'has_change', 'rationales', 'link', 'all_attributes'];
const defaultWidths = {
'product_id': 120,
'name': 200,
'norm_value': 150,
'current_value': 150,
'has_change': 100,
'rationales': 300,
'link': 200,
'all_attributes': 300
};
setColumns(defaultCols);
setOriginalColumns(defaultCols);
setSpreadsheetData([]);
setColumnWidths(defaultWidths);
if (!regions[0]) {
addRegion({
changes: {
cellEdits: [],
addedRows: [],
deletedRows: [],
addedColumns: [],
deletedColumns: []
}
});
}
}
}, [data]);
// Get current rows by applying changes to original rows
const currentRows = React.useMemo(() => {
let rows = [...originalRows];
// Remove deleted rows first
const deletedProductIds = new Set(changes.deletedRows.map(d => d.productId));
rows = rows.filter(row => !deletedProductIds.has(row.product_id));
// Add new rows first
rows = [...rows, ...changes.addedRows];
// Apply cell edits (using product_id or _tempId to find the row)
changes.cellEdits.forEach(edit => {
const rowIndex = rows.findIndex(r =>
(r.product_id && r.product_id === edit.productId) ||
(r._tempId && r._tempId === edit.productId)
);
if (rowIndex >= 0) {
rows[rowIndex] = { ...rows[rowIndex], [edit.column]: edit.newValue };
}
});
return rows;
}, [originalRows, changes]);
const currentColumns = state.columns && state.columns.length > 0 ? state.columns : columns;
const currentOriginalColumns = state.originalColumns && state.originalColumns.length > 0 ? state.originalColumns : originalColumns;
const currentColumnWidths = state.columnWidths || columnWidths;
const currentColumnFilters = state.columnFilters || columnFilters;
// Helper function to save only changes to region
const saveChanges = (updatedChanges) => {
const changesToSave = {
changes: updatedChanges
};
if (regions[0]) {
regions[0].update(changesToSave);
} else {
addRegion(changesToSave);
}
setChanges(updatedChanges);
};
// Filter rows based on filter text and column-specific filters
const filteredRows = React.useMemo(() => {
let filtered = currentRows;
// Apply global filter if present
if (filterText.trim()) {
const searchText = filterText.toLowerCase();
filtered = filtered.filter(row => {
return currentColumns.some(col => {
const value = row[col];
if (value === null || value === undefined) return false;
const valueStr = typeof value === 'object' ? JSON.stringify(value) : String(value);
return valueStr.toLowerCase().includes(searchText);
});
});
}
// Apply column-specific filters
const activeColumnFilters = Object.entries(currentColumnFilters).filter(([_, filterValue]) => filterValue && filterValue.trim());
if (activeColumnFilters.length > 0) {
filtered = filtered.filter(row => {
return activeColumnFilters.every(([colName, filterValue]) => {
const value = row[colName];
if (value === null || value === undefined) return false;
const valueStr = typeof value === 'object' ? JSON.stringify(value) : String(value);
return valueStr.toLowerCase().includes(filterValue.toLowerCase());
});
});
}
return filtered;
}, [currentRows, filterText, currentColumns, currentColumnFilters]);
// Add new row
const addRow = () => {
const newRow = {};
currentColumns.forEach(col => {
newRow[col] = '';
});
// Add temporary ID for tracking
newRow._tempId = `temp_${Date.now()}_${Math.random()}`;
const updatedChanges = {
...changes,
addedRows: [...changes.addedRows, newRow]
};
saveChanges(updatedChanges);
setSpreadsheetData([...currentRows, newRow]);
};
// Delete row
const deleteRow = (rowIndex) => {
const rowToDelete = currentRows[rowIndex];
// Check if it's a newly added row (has temp ID)
if (rowToDelete._tempId) {
// Remove from addedRows
const updatedChanges = {
...changes,
addedRows: changes.addedRows.filter(r => r._tempId !== rowToDelete._tempId)
};
saveChanges(updatedChanges);
} else {
// It's an original row - add to deletedRows
const updatedChanges = {
...changes,
deletedRows: [...changes.deletedRows, {
productId: rowToDelete.product_id,
rowData: { ...rowToDelete }
}]
};
saveChanges(updatedChanges);
}
const newRows = currentRows.filter((_, idx) => idx !== rowIndex);
setSpreadsheetData(newRows);
};
// Add new column
const addColumn = () => {
if (!newColumnName.trim()) {
alert('Please enter a column name');
return;
}
if (currentColumns.includes(newColumnName.trim())) {
alert('Column already exists');
return;
}
const newCols = [...currentColumns, newColumnName.trim()];
// Add empty value for this column to all existing rows
const newRows = currentRows.map(row => ({
...row,
[newColumnName.trim()]: ''
}));
// Add default width for new column
const newWidths = { ...currentColumnWidths, [newColumnName.trim()]: 150 };
// Track new column addition
const updatedChanges = {
...changes,
addedColumns: [...changes.addedColumns, newColumnName.trim()]
};
saveChanges(updatedChanges);
setColumns(newCols);
setColumnWidths(newWidths);
setSpreadsheetData(newRows);
setNewColumnName('');
};
// Delete column (only for new columns)
const deleteColumn = (colName) => {
// Check if it's an original column
if (currentOriginalColumns.includes(colName)) {
alert('Cannot delete original columns. Only newly added columns can be deleted.');
return;
}
if (currentColumns.length <= 1) {
alert('Cannot delete the last column');
return;
}
const newCols = currentColumns.filter(col => col !== colName);
const newRows = currentRows.map(row => {
const newRow = { ...row };
delete newRow[colName];
return newRow;
});
// Remove width and filter for deleted column
const newWidths = { ...currentColumnWidths };
delete newWidths[colName];
const newFilters = { ...currentColumnFilters };
delete newFilters[colName];
// Track column deletion
const updatedChanges = {
...changes,
deletedColumns: [...changes.deletedColumns, colName],
// Also remove any cell edits for this column
cellEdits: changes.cellEdits.filter(e => e.column !== colName)
};
saveChanges(updatedChanges);
setColumns(newCols);
setColumnWidths(newWidths);
setColumnFilters(newFilters);
setSpreadsheetData(newRows);
};
// Update cell value
const updateCell = (rowIndex, colName, value) => {
const row = currentRows[rowIndex];
const oldValue = row[colName];
const rowId = row.product_id || row._tempId;
// Find original value for this cell
let originalValue = oldValue;
if (row.product_id && !row._tempId) {
// It's an original row - find it in originalRows
const originalRow = originalRows.find(r => r.product_id === row.product_id);
if (originalRow) {
originalValue = originalRow[colName];
}
}
// Check if this is actually a change from original
const isNewRow = row._tempId !== undefined;
const isActualChange = !isNewRow && JSON.stringify(originalValue) !== JSON.stringify(value);
if (isNewRow || isActualChange) {
// Check if we already have an edit for this cell (using productId/tempId)
const existingEditIndex = changes.cellEdits.findIndex(
e => e.productId === rowId && e.column === colName
);
let updatedEdits = [...changes.cellEdits];
if (existingEditIndex >= 0) {
// Update existing edit
updatedEdits[existingEditIndex] = {
...updatedEdits[existingEditIndex],
newValue: value
};
} else {
// Add new edit
updatedEdits.push({
column: colName,
oldValue: isNewRow ? oldValue : originalValue,
newValue: value,
productId: rowId
});
}
const updatedChanges = {
...changes,
cellEdits: updatedEdits
};
saveChanges(updatedChanges);
}
const newRows = [...currentRows];
if (!newRows[rowIndex]) {
newRows[rowIndex] = {};
}
newRows[rowIndex] = { ...newRows[rowIndex], [colName]: value };
setSpreadsheetData(newRows);
};
// Handle column reordering
const handleColumnDragStart = (colIndex) => {
setDraggedColumn(colIndex);
};
const handleColumnDragOver = (e, colIndex) => {
e.preventDefault();
if (draggedColumn === null || draggedColumn === colIndex) return;
const newColumns = [...currentColumns];
const draggedCol = newColumns[draggedColumn];
newColumns.splice(draggedColumn, 1);
newColumns.splice(colIndex, 0, draggedCol);
setColumns(newColumns);
setDraggedColumn(colIndex);
};
const handleColumnDragEnd = () => {
// Column reordering is UI-only, doesn't need to be saved as a change
setDraggedColumn(null);
};
// Handle column width resizing
const handleResizeStart = (e, colName) => {
e.preventDefault();
e.stopPropagation();
setResizingColumn(colName);
setResizeStartX(e.clientX);
setResizeStartWidth(currentColumnWidths[colName] || 150);
};
React.useEffect(() => {
if (!resizingColumn) return;
const handleResize = (e) => {
const diff = e.clientX - resizeStartX;
const newWidth = Math.max(50, resizeStartWidth + diff);
setColumnWidths(prev => ({ ...prev, [resizingColumn]: newWidth }));
};
const handleResizeEnd = () => {
// Column width resizing is UI-only, doesn't need to be saved as a change
setResizingColumn(null);
};
document.addEventListener('mousemove', handleResize);
document.addEventListener('mouseup', handleResizeEnd);
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleResize);
document.removeEventListener('mouseup', handleResizeEnd);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [resizingColumn, resizeStartX, resizeStartWidth, state, currentColumnFilters, regions]);
// Update column filter
const updateColumnFilter = (colName, filterValue) => {
// Filtering is UI-only, doesn't need to be saved as a change
const newFilters = { ...currentColumnFilters, [colName]: filterValue };
setColumnFilters(newFilters);
};
// Handle submit
const handleSubmit = () => {
// Final submission - save changes with metadata
const submission = {
changes: {
...changes,
submittedAt: new Date().toISOString(),
submitted: true
}
};
if (regions[0]) {
regions[0].update(submission);
} else {
addRegion(submission);
}
const changeCount = changes.cellEdits.length + changes.addedRows.length + changes.deletedRows.length;
alert(`Spreadsheet submitted successfully! ${changeCount} change(s) recorded.`);
};
// Styles
const containerStyle = {
padding: '20px',
fontFamily: 'Arial, sans-serif',
maxWidth: '100%',
overflowX: 'auto'
};
const headerStyle = {
marginBottom: '20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '10px'
};
const titleStyle = {
fontSize: '24px',
fontWeight: 'bold',
color: '#333',
margin: 0
};
const buttonStyle = {
padding: '10px 20px',
fontSize: '14px',
fontWeight: 'bold',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
marginRight: '10px',
backgroundColor: '#2196F3',
color: 'white'
};
const addColumnStyle = {
...buttonStyle,
backgroundColor: '#4CAF50'
};
const deleteButtonStyle = {
...buttonStyle,
backgroundColor: '#f44336',
padding: '5px 10px',
fontSize: '12px',
marginRight: '5px'
};
const submitButtonStyle = {
...buttonStyle,
backgroundColor: '#4CAF50',
fontSize: '16px',
padding: '12px 24px'
};
const tableContainerStyle = {
overflowX: 'auto',
border: '1px solid #ddd',
borderRadius: '8px',
marginBottom: '20px'
};
const tableStyle = {
width: '100%',
borderCollapse: 'collapse',
backgroundColor: 'white',
minWidth: '800px'
};
const thStyle = {
backgroundColor: '#f5f5f5',
padding: '12px',
textAlign: 'left',
borderBottom: '2px solid #ddd',
borderRight: '1px solid #ddd',
fontWeight: 'bold',
color: '#333',
position: 'sticky',
top: 0,
zIndex: 10
};
const tdStyle = {
padding: '10px',
borderBottom: '1px solid #eee',
borderRight: '1px solid #eee',
fontSize: '14px'
};
const inputStyle = {
width: '100%',
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '14px',
boxSizing: 'border-box'
};
const textareaStyle = {
...inputStyle,
minHeight: '60px',
resize: 'vertical',
fontFamily: 'inherit'
};
const addColumnContainerStyle = {
display: 'flex',
gap: '10px',
alignItems: 'center',
marginBottom: '20px',
padding: '15px',
backgroundColor: '#f9f9f9',
borderRadius: '6px'
};
const columnInputStyle = {
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '14px',
flex: '1',
maxWidth: '300px'
};
const rowActionsStyle = {
display: 'flex',
gap: '5px'
};
const emptyStateStyle = {
textAlign: 'center',
padding: '40px',
color: '#999'
};
const filterContainerStyle = {
marginBottom: '20px',
padding: '15px',
backgroundColor: '#f9f9f9',
borderRadius: '6px',
display: 'flex',
gap: '10px',
alignItems: 'center'
};
const filterInputStyle = {
padding: '10px',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '14px',
flex: '1',
maxWidth: '400px'
};
return React.createElement("div", { style: containerStyle },
// Header
React.createElement("div", { style: headerStyle },
React.createElement("h1", { style: titleStyle }, "Spreadsheet Editor"),
React.createElement("div", {},
React.createElement("button", {
onClick: addRow,
style: buttonStyle
}, "+ Add Row"),
React.createElement("button", {
onClick: handleSubmit,
style: submitButtonStyle
}, "Submit")
)
),
// Filter Section
currentRows.length > 0 && React.createElement("div", { style: filterContainerStyle },
React.createElement("label", { style: { fontWeight: 'bold', fontSize: '14px', color: '#555' } }, "Global Filter:"),
React.createElement("input", {
type: "text",
value: filterText,
onChange: (e) => setFilterText(e.target.value),
placeholder: "Search across all columns...",
style: filterInputStyle
}),
filterText && React.createElement("button", {
onClick: () => setFilterText(''),
style: { ...buttonStyle, backgroundColor: '#999', padding: '10px 15px' }
}, "Clear")
),
// Add Column Section
React.createElement("div", { style: addColumnContainerStyle },
React.createElement("input", {
type: "text",
value: newColumnName,
onChange: (e) => setNewColumnName(e.target.value),
placeholder: "Enter new column name",
style: columnInputStyle,
onKeyPress: (e) => {
if (e.key === 'Enter') {
addColumn();
}
}
}),
React.createElement("button", {
onClick: addColumn,
style: addColumnStyle
}, "+ Add Column")
),
// Spreadsheet Table
currentRows.length > 0 ? React.createElement("div", { style: tableContainerStyle },
React.createElement("table", { style: tableStyle },
// Header Row
React.createElement("thead", {},
React.createElement("tr", {},
React.createElement("th", { style: { ...thStyle, width: '80px' } }, "Actions"),
currentColumns.map((col, colIdx) =>
React.createElement("th", {
key: colIdx,
style: {
...thStyle,
width: currentColumnWidths[col] || 150,
minWidth: currentColumnWidths[col] || 150,
maxWidth: currentColumnWidths[col] || 150,
position: 'relative',
userSelect: 'none',
opacity: draggedColumn === colIdx ? 0.5 : 1,
backgroundColor: draggedColumn === colIdx ? '#e3f2fd' : thStyle.backgroundColor
},
draggable: true,
onDragStart: () => handleColumnDragStart(colIdx),
onDragOver: (e) => handleColumnDragOver(e, colIdx),
onDragEnd: handleColumnDragEnd
},
React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: '5px' } },
// Column header with drag handle
React.createElement("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '5px', flex: 1, cursor: 'move' } },
React.createElement("span", { style: { fontSize: '10px', color: '#999', cursor: 'grab' } }, "โฎโฎ"),
React.createElement("span", {}, col)
),
// Only show delete button for new columns (not original columns)
!currentOriginalColumns.includes(col) && React.createElement("button", {
onClick: () => deleteColumn(col),
style: { ...deleteButtonStyle, padding: '2px 8px', fontSize: '10px' },
title: "Delete column"
}, "ร")
),
// Column filter input
React.createElement("input", {
type: "text",
value: currentColumnFilters[col] || '',
onChange: (e) => updateColumnFilter(col, e.target.value),
placeholder: `Filter ${col}...`,
style: {
padding: '4px 8px',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '12px',
width: '100%',
boxSizing: 'border-box'
},
onClick: (e) => e.stopPropagation()
})
),
// Resize handle
React.createElement("div", {
onMouseDown: (e) => handleResizeStart(e, col),
style: {
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: '5px',
cursor: 'col-resize',
backgroundColor: resizingColumn === col ? '#2196F3' : 'transparent',
zIndex: 20
},
title: "Drag to resize column"
})
)
)
)
),
// Data Rows (using filtered rows)
React.createElement("tbody", {},
filteredRows.map((row, filteredIdx) => {
// Find the actual index in currentRows by matching the row object reference or key fields
let actualRowIndex = currentRows.findIndex(r => r === row);
// If reference match fails, try matching by key fields
if (actualRowIndex < 0 && row.product_id && row.name) {
actualRowIndex = currentRows.findIndex(r =>
r.product_id === row.product_id && r.name === row.name
);
}
// Final fallback: use filtered index (shouldn't happen in normal cases)
const rowIdx = actualRowIndex >= 0 ? actualRowIndex : filteredIdx;
return React.createElement("tr", { key: filteredIdx },
// Actions column
React.createElement("td", { style: tdStyle },
React.createElement("div", { style: rowActionsStyle },
React.createElement("button", {
onClick: () => deleteRow(rowIdx),
style: deleteButtonStyle,
title: "Delete row"
}, "Delete")
)
),
// Data cells
currentColumns.map((col, colIdx) => {
let cellValue = row[col];
// Handle all_attributes field - display as JSON
if (col === 'all_attributes') {
if (cellValue === null || cellValue === undefined) {
cellValue = '';
} else if (typeof cellValue === 'object') {
cellValue = JSON.stringify(cellValue, null, 2);
} else {
cellValue = String(cellValue);
}
} else {
cellValue = cellValue || '';
}
const isEditing = editingCell && editingCell.row === rowIdx && editingCell.col === col;
return React.createElement("td", {
key: colIdx,
style: {
...tdStyle,
width: currentColumnWidths[col] || 150,
minWidth: currentColumnWidths[col] || 150,
maxWidth: currentColumnWidths[col] || 150
}
},
(col === 'rationales' || col === 'all_attributes') ? (
React.createElement("textarea", {
value: typeof cellValue === 'object' ? JSON.stringify(cellValue, null, 2) : (cellValue || ''),
onChange: (e) => {
if (col === 'all_attributes') {
try {
const parsed = JSON.parse(e.target.value);
updateCell(rowIdx, col, parsed);
} catch (err) {
updateCell(rowIdx, col, e.target.value);
}
} else {
try {
const parsed = JSON.parse(e.target.value);
updateCell(rowIdx, col, parsed);
} catch (err) {
updateCell(rowIdx, col, e.target.value);
}
}
},
style: textareaStyle,
placeholder: "Enter value"
})
) : (
React.createElement("input", {
type: "text",
value: cellValue || '',
onChange: (e) => updateCell(rowIdx, col, e.target.value),
style: inputStyle,
placeholder: "Enter value"
})
)
);
})
);
})
)
)
) : React.createElement("div", { style: emptyStateStyle },
currentRows.length === 0 ? (
React.createElement(React.Fragment, {},
React.createElement("p", { style: { fontSize: '18px', marginBottom: '10px' } }, "No data available"),
React.createElement("p", { style: { fontSize: '14px', color: '#999' } }, "Click 'Add Row' to start adding data")
)
) : (
React.createElement(React.Fragment, {},
React.createElement("p", { style: { fontSize: '18px', marginBottom: '10px' } }, "No rows match the filter"),
React.createElement("p", { style: { fontSize: '14px', color: '#999' } }, `Try adjusting your search. Total rows: ${currentRows.length}`)
)
)
),
// Summary
currentRows.length > 0 && React.createElement("div", { style: { marginTop: '20px', padding: '15px', backgroundColor: '#f9f9f9', borderRadius: '6px' } },
React.createElement("p", { style: { margin: 0, fontSize: '14px', color: '#666' } },
filterText ?
`Showing ${filteredRows.length} of ${currentRows.length} rows | Total Columns: ${currentColumns.length}` :
`Total Rows: ${currentRows.length} | Total Columns: ${currentColumns.length}`
)
)
);
}
]]>
</ReactCode>
</View>
Example input
Copy this into a JSON file and then import it into a project with the example code above.
This will create three rows in your spreadsheet editor.
Click to expand
{
"data": {
"attributes_data": [
{
"link": "https://www.example-store.com/products/WM-2024-001",
"name": "Wireless Connectivity Range",
"product_id": "WM-2024-001",
"has_change": false,
"norm_value": null,
"rationales": "{\"input_attributes\": [{\"Wireless Connectivity Range\": \"10 meters\"}], \"input_rules\": [\"Wireless Connectivity Range: The maximum distance the device can maintain a stable connection from the receiver.\"], \"other\": \"The input explicitly states the Wireless Connectivity Range as '10 meters'.\"}",
"current_value": "10 meters",
"all_attributes": {
"Item": "Wireless Mouse",
"Connectivity": "2.4GHz Wireless",
"Battery Life": "12 months",
"For Use With": "Desktop, Laptop",
"Standards": "CE, FCC Certified",
"Mouse Type": "Optical",
"Buttons": "3-Button",
"Body Material": "Plastic",
"Grip Material": "Rubber",
"Scroll Wheel": "Yes, with tilt",
"Operation Type": "Wireless",
"Overall Height": "1.5 in",
"Overall Length": "4.2 in",
"Overall Width": "2.5 in",
"Weight": "85g",
"Mounting Orientation": "Right or Left Hand",
"DPI Range": "800-1600",
"Connection Type": "USB Receiver",
"Compatibility": "Windows, macOS, Linux",
"Wireless Range": "10 meters",
"Battery Type": "AA",
"Color": "Black",
"Package Contents": "Mouse, USB Receiver, Battery",
"Warranty Period": "2 years",
"Operating Temperature - Maximum": "40ยฐC",
"Operating Temperature - Minimum": "0ยฐC",
"Storage Humidity - Maximum": "85%",
"Storage Humidity - Minimum": "10%"
}
},
{
"link": "https://www.example-store.com/products/WM-2024-002",
"name": "Battery Life Duration",
"product_id": "WM-2024-002",
"has_change": false,
"norm_value": null,
"rationales": "{\"input_attributes\": [{\"Battery Life Duration\": \"18 months\"}], \"input_rules\": [\"Battery Life Duration: The expected operating time before battery replacement is needed under normal usage.\"], \"other\": \"The input explicitly states the Battery Life Duration as '18 months'.\"}",
"current_value": "18 months",
"all_attributes": {
"Item": "Ergonomic Wireless Mouse",
"Connectivity": "Bluetooth 5.0",
"Battery Life": "18 months",
"For Use With": "Desktop, Laptop, Tablet",
"Standards": "CE, FCC, RoHS Certified",
"Mouse Type": "Laser",
"Buttons": "5-Button",
"Body Material": "ABS Plastic",
"Grip Material": "Silicone",
"Scroll Wheel": "Yes, with horizontal tilt",
"Operation Type": "Wireless",
"Overall Height": "1.8 in",
"Overall Length": "4.8 in",
"Overall Width": "3.0 in",
"Weight": "105g",
"Mounting Orientation": "Right Hand",
"DPI Range": "1000-3200",
"Connection Type": "Bluetooth or USB Receiver",
"Compatibility": "Windows, macOS, Linux, Android",
"Wireless Range": "15 meters",
"Battery Type": "Rechargeable Lithium",
"Color": "Silver",
"Package Contents": "Mouse, USB-C Cable, USB Receiver, Quick Start Guide",
"Warranty Period": "3 years",
"Operating Temperature - Maximum": "45ยฐC",
"Operating Temperature - Minimum": "-5ยฐC",
"Storage Humidity - Maximum": "90%",
"Storage Humidity - Minimum": "5%"
}
},
{
"link": "https://www.example-store.com/products/WM-2024-003",
"name": "DPI Sensitivity Setting",
"product_id": "WM-2024-003",
"has_change": true,
"norm_value": "1600",
"rationales": "{\"input_attributes\": [{\"DPI Sensitivity Setting\": \"2400\"}], \"input_rules\": [\"DPI Sensitivity Setting: The dots per inch sensitivity level for cursor movement precision.\"], \"other\": \"The input shows a DPI setting of '2400', which differs from the normalized value of '1600'.\"}",
"current_value": "2400",
"all_attributes": {
"Item": "Gaming Wireless Mouse",
"Connectivity": "2.4GHz Wireless + Bluetooth",
"Battery Life": "6 months",
"For Use With": "Gaming PC, Laptop",
"Standards": "CE, FCC Certified",
"Mouse Type": "Optical Gaming Sensor",
"Buttons": "6-Button Programmable",
"Body Material": "Matte Plastic",
"Grip Material": "Textured Rubber",
"Scroll Wheel": "Yes, RGB Backlit",
"Operation Type": "Wireless",
"Overall Height": "1.6 in",
"Overall Length": "5.0 in",
"Overall Width": "2.8 in",
"Weight": "120g",
"Mounting Orientation": "Right Hand",
"DPI Range": "800-4000",
"Connection Type": "USB Receiver",
"Compatibility": "Windows, macOS",
"Wireless Range": "12 meters",
"Battery Type": "AA",
"Color": "RGB Customizable",
"Package Contents": "Mouse, USB Receiver, Battery, Software CD",
"Warranty Period": "1 year",
"Operating Temperature - Maximum": "50ยฐC",
"Operating Temperature - Minimum": "0ยฐC",
"Storage Humidity - Maximum": "80%",
"Storage Humidity - Minimum": "10%"
}
}
]
}
}
Example output
The output only reflects edits made to the spreadsheet. For example, in this annotation the user added values to the previously empty norm_value cells:
{
"result": [
{
"id": "eRb3JW4K72",
"type": "reactcode",
"value": {
"reactcode": {
"changes": {
"addedRows": [],
"cellEdits": [
{
"column": "norm_value",
"newValue": "1200",
"oldValue": null,
"productId": "WM-2024-001"
},
{
"column": "norm_value",
"newValue": "1400",
"oldValue": null,
"productId": "WM-2024-002"
}
],
"deletedRows": [],
"addedColumns": [],
"deletedColumns": []
}
}
},
"origin": "manual",
"to_name": "spreadsheet_editor",
"from_name": "spreadsheet_editor"
}
]
}