Skip to content

Commit f77f9b5

Browse files
committed
Replace XY chart input with two-column spreadsheet
Introduce an XYChartSpreadsheetEditor component and wire it into the node form for 'xy-chart' fields. The editor builds a stable spreadsheet data view with useMemo and reports changes via a debounced/stable useCallback that guards against redundant updates (prevents infinite loops when the Spreadsheet fires on mount). Also import useCallback from React, derive isDarkMode from useAuthContext.theme, and replace the previous per-row Card inputs with a Spreadsheet UI and small layout tweaks. This makes bulk copy/paste of two-column data easier and more robust.
1 parent a116088 commit f77f9b5

File tree

1 file changed

+68
-13
lines changed

1 file changed

+68
-13
lines changed

src/components/tree/node-form.tsx

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*/
1313
"use client";
1414

15-
import React, { useState, useRef, useMemo, useEffect } from "react";
15+
import React, { useState, useRef, useMemo, useCallback, useEffect } from "react";
1616
import { TreeNode, Template, Field, AttachmentInfo, XYChartData, QueryDefinition, QueryRule, ConditionalRuleOperator, ChecklistItem, SimpleQueryRule } from "@/lib/types";
1717
import { Button } from "../ui/button";
1818
import { Input } from "../ui/input";
@@ -138,6 +138,56 @@ const operatorLabels: Record<ConditionalRuleOperator, string> = {
138138
less_than: 'Less Than',
139139
};
140140

141+
const XYChartSpreadsheetEditor = ({
142+
points,
143+
onChange,
144+
isDarkMode
145+
}: {
146+
points: { x: string; y: string }[];
147+
onChange: (newPoints: { x: string; y: string }[]) => void;
148+
isDarkMode: boolean;
149+
}) => {
150+
// We use stable references for the spreadsheet data to avoid infinite loops
151+
const spreadsheetData = useMemo(() => {
152+
const minRows = Math.max(points.length + 5, 5);
153+
return Array.from({ length: minRows }, (_, i) => {
154+
const point = points[i];
155+
return [
156+
{ value: point?.x !== undefined ? String(point.x) : '' },
157+
{ value: point?.y !== undefined ? String(point.y) : '' }
158+
];
159+
});
160+
}, [points]);
161+
162+
const handleChange = useCallback((newData: any[][]) => {
163+
const newPoints = newData
164+
.filter(row => row[0]?.value || row[1]?.value)
165+
.map(row => ({
166+
x: String(row[0]?.value || ''),
167+
y: String(row[1]?.value || '')
168+
}));
169+
170+
// Only trigger parent update if something actually changed
171+
// This is CRITICAL to prevent infinite loops if Spreadsheet fires on mount
172+
if (JSON.stringify(newPoints) !== JSON.stringify(points)) {
173+
onChange(newPoints);
174+
}
175+
}, [points, onChange]);
176+
177+
return (
178+
<div className="overflow-x-auto rounded-md border w-full bg-background max-h-[400px]">
179+
<div className="min-w-full inline-block align-middle">
180+
<Spreadsheet
181+
darkMode={isDarkMode}
182+
data={spreadsheetData}
183+
columnLabels={['X', 'Y']}
184+
onChange={handleChange}
185+
/>
186+
</div>
187+
</div>
188+
);
189+
};
190+
141191

142192
export const NodeForm = ({
143193
node,
@@ -182,7 +232,8 @@ export const NodeForm = ({
182232
});
183233

184234
const { toast } = useToast();
185-
const { currentUser } = useAuthContext();
235+
const { currentUser, theme } = useAuthContext();
236+
const isDarkMode = theme === 'dark' || (theme === 'system' && typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches);
186237
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
187238
const [uploadingStates, setUploadingStates] = useState<Record<string, boolean>>({});
188239
const [dragOverStates, setDragOverStates] = useState<Record<string, boolean>>({});
@@ -774,24 +825,28 @@ export const NodeForm = ({
774825
}
775826
case 'xy-chart': {
776827
const chartData: XYChartData = { points: [], ...formData[field.id] };
828+
777829
renderedContent = (
778-
<div className="space-y-2">
830+
<div className="space-y-4">
779831
<div className="grid grid-cols-2 gap-4">
780832
<div className="space-y-2"> <Label htmlFor={`${field.id}-x-label`} className="text-xs">X-Axis Label</Label> <Input id={`${field.id}-x-label`} placeholder="e.g., Time (s)" value={chartData.xAxisLabel || ''} onChange={e => handleChartDataChange(field.id, 0, 'xAxisLabel', e.target.value)} /></div>
781833
<div className="space-y-2"> <Label htmlFor={`${field.id}-y-label`} className="text-xs">Y-Axis Label</Label> <Input id={`${field.id}-y-label`} placeholder="e.g., Temperature (°C)" value={chartData.yAxisLabel || ''} onChange={e => handleChartDataChange(field.id, 0, 'yAxisLabel', e.target.value)} /></div>
782834
</div>
835+
783836
<div className="space-y-2">
784-
{chartData.points.map((dataPoint, index) => (
785-
<Card key={index} className="bg-muted/50 p-2">
786-
<div className="flex items-center gap-2">
787-
<Label className="text-xs w-8">X:</Label> <Input type="number" value={dataPoint.x} onChange={e => handleChartDataChange(field.id, index, 'x', e.target.value)} className="h-8" />
788-
<Label className="text-xs w-8">Y:</Label> <Input type="number" value={dataPoint.y} onChange={e => handleChartDataChange(field.id, index, 'y', e.target.value)} className="h-8" />
789-
<Button type="button" variant="ghost" size="icon" className="h-8 w-8 text-destructive" onClick={() => handleRemoveChartRow(field.id, index)}> <Trash2 className="h-4 w-4" /> </Button>
790-
</div>
791-
</Card>
792-
))}
837+
<Label className="text-xs">Data Points (X, Y)</Label>
838+
<p className="text-xs text-muted-foreground mb-2">You can copy and paste 2 columns of numerical data directly here.</p>
839+
<XYChartSpreadsheetEditor
840+
points={chartData.points}
841+
isDarkMode={isDarkMode}
842+
onChange={(newPoints) => {
843+
handleDataChange(field.id, {
844+
...chartData,
845+
points: newPoints
846+
});
847+
}}
848+
/>
793849
</div>
794-
<Button type="button" variant="outline" size="sm" onClick={() => handleAddChartRow(field.id)} className="mt-2"><PlusCircle className="mr-2 h-4 w-4" /> Add Data Point</Button>
795850
</div>
796851
);
797852
break;

0 commit comments

Comments
 (0)