返回 presentation-ai
chart-data-editor-dialog.tsx
根目录 / src / components / notebook / presentation / editor / custom-elements / chart-data-editor-dialog.tsx
1 "use client";
2
3 import { FileSpreadsheet, X } from "lucide-react";
4 import { useEffect, useRef, useState } from "react";
5
6 import {
7 PRESENTATION_PORTAL_CONTENT_CLASS,
8 PRESENTATION_PORTAL_OVERLAY_CLASS,
9 } from "@/components/presentation/overlay-layers";
10 import { Button } from "@/components/ui/button";
11 import {
12 Credenza,
13 CredenzaContent,
14 CredenzaDescription,
15 CredenzaFooter,
16 CredenzaHeader,
17 CredenzaTitle,
18 } from "@/components/ui/credenza";
19 import {
20 ChartDataEditor,
21 type ChartDataMode,
22 type ChartDataType,
23 type SeriesChartType,
24 } from "./chart-data-editor";
25 import { importChartDataFromFile } from "./chart-data-editor/import-data";
26 import { ChartRenderer } from "./charts/ChartRenderer";
27
28 interface ChartDataEditorDialogProps {
29 open: boolean;
30 onOpenChange: (open: boolean) => void;
31 data: ChartDataType;
32 onDataChange: (data: ChartDataType) => void;
33 chartType: ChartDataMode;
34 title?: string;
35 isComposedChart?: boolean;
36 seriesChartTypes?: Record<string, SeriesChartType>;
37 onSeriesChartTypesChange?: (types: Record<string, SeriesChartType>) => void;
38 previewChartType?: string;
39 chartOptions?: Record<string, unknown>;
40 }
41
42 export function ChartDataEditorDialog({
43 open,
44 onOpenChange,
45 data,
46 onDataChange,
47 chartType,
48 title = "Edit Chart Data",
49 isComposedChart = false,
50 seriesChartTypes: initialSeriesChartTypes,
51 onSeriesChartTypesChange,
52 previewChartType,
53 chartOptions,
54 }: ChartDataEditorDialogProps) {
55 const [localData, setLocalData] = useState<ChartDataType>(data);
56 const [localSeriesChartTypes, setLocalSeriesChartTypes] = useState<
57 Record<string, SeriesChartType>
58 >(initialSeriesChartTypes ?? {});
59 const [importVersion, setImportVersion] = useState(0);
60 const [importError, setImportError] = useState<string | null>(null);
61 const fileInputRef = useRef<HTMLInputElement | null>(null);
62
63 // Sync with external data when dialog opens
64 useEffect(() => {
65 if (open) {
66 setLocalData(data);
67 setLocalSeriesChartTypes(initialSeriesChartTypes ?? {});
68 setImportError(null);
69 setImportVersion((version) => version + 1);
70 }
71 }, [open, data, initialSeriesChartTypes]);
72
73 const handleSave = () => {
74 onDataChange(localData);
75 if (isComposedChart && onSeriesChartTypesChange) {
76 onSeriesChartTypesChange(localSeriesChartTypes);
77 }
78 onOpenChange(false);
79 };
80
81 const handleCancel = () => {
82 setLocalData(data);
83 setLocalSeriesChartTypes(initialSeriesChartTypes ?? {});
84 onOpenChange(false);
85 };
86
87 const handleDataChange = (newData: ChartDataType) => {
88 setLocalData(newData);
89 };
90
91 const handleSeriesChartTypesChange = (
92 types: Record<string, SeriesChartType>,
93 ) => {
94 setLocalSeriesChartTypes(types);
95 };
96
97 const handleImportFile = async (file: File) => {
98 try {
99 const importedData = await importChartDataFromFile(file, chartType);
100 setLocalData(importedData);
101 setImportVersion((version) => version + 1);
102 setImportError(null);
103 } catch (error) {
104 try {
105 setImportError(
106 error instanceof Error
107 ? error.message
108 : "Unable to import this file.",
109 );
110 } catch (reactDoctorCatchError) {
111 if (fileInputRef.current) {
112 fileInputRef.current.value = "";
113 }
114 throw reactDoctorCatchError;
115 }
116 }
117 if (fileInputRef.current) {
118 fileInputRef.current.value = "";
119 }
120 };
121
122 const previewType = previewChartType ?? "";
123 const previewOptions = {
124 ...(chartOptions ?? {}),
125 seriesChartTypes: localSeriesChartTypes,
126 previewMode: true,
127 };
128
129 return (
130 <Credenza open={open} onOpenChange={onOpenChange}>
131 <CredenzaContent
132 overlayClassName={PRESENTATION_PORTAL_OVERLAY_CLASS}
133 shouldHaveClose={false}
134 className={`${PRESENTATION_PORTAL_CONTENT_CLASS} ignore-click-outside/toolbar flex h-[92dvh] max-h-[92dvh] w-screen max-w-none flex-col overflow-hidden rounded-t-xl p-0 sm:rounded-xl md:h-[calc(100dvh-3rem)] md:max-h-[calc(100dvh-3rem)] md:rounded-xl`}
135 >
136 <CredenzaHeader className="shrink-0 border-b px-4 py-3 sm:px-5 sm:py-4">
137 <div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
138 <div className="min-w-0">
139 <CredenzaTitle>{title}</CredenzaTitle>
140 <CredenzaDescription className="line-clamp-2">
141 Edit the data and preview the chart before saving.
142 </CredenzaDescription>
143 </div>
144 <div className="flex shrink-0 items-center gap-2">
145 <input
146 aria-label="chart data editor dialog control"
147 ref={fileInputRef}
148 type="file"
149 accept=".csv,text/csv,.xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
150 className="hidden"
151 onChange={(event) => {
152 const file = event.target.files?.[0];
153 if (file) void handleImportFile(file);
154 }}
155 />
156 <Button
157 variant="outline"
158 className="hidden gap-2 sm:inline-flex"
159 onClick={() => fileInputRef.current?.click()}
160 >
161 <FileSpreadsheet className="size-4" />
162 Import CSV/XLSX
163 </Button>
164 <Button
165 variant="outline"
166 className="size-9 p-0 sm:hidden"
167 onClick={() => fileInputRef.current?.click()}
168 aria-label="Import CSV or Excel data"
169 >
170 <FileSpreadsheet className="size-4" />
171 </Button>
172 <Button
173 variant="ghost"
174 className="size-9 p-0"
175 onClick={handleCancel}
176 aria-label="Close chart data editor"
177 >
178 <X className="size-4" />
179 </Button>
180 </div>
181 </div>
182 </CredenzaHeader>
183
184 <div className="grid min-h-0 flex-1 grid-rows-[minmax(220px,34dvh)_minmax(0,1fr)] overflow-hidden lg:grid-cols-[minmax(320px,0.9fr)_minmax(460px,1.1fr)] lg:grid-rows-none">
185 <section className="min-h-0 border-b bg-muted/20 p-3 sm:p-4 lg:border-r lg:border-b-0 lg:p-5">
186 <div className="h-full min-h-0 overflow-hidden rounded-lg border bg-background p-2 sm:p-3">
187 {previewType ? (
188 <ChartRenderer
189 chartType={previewType}
190 chartData={localData}
191 chartOptions={previewOptions}
192 className="h-full min-h-0 border-0 shadow-none"
193 />
194 ) : (
195 <div className="flex h-full items-center justify-center text-sm text-muted-foreground">
196 Chart preview unavailable.
197 </div>
198 )}
199 </div>
200 </section>
201
202 <section className="min-h-0 overflow-auto p-3 sm:p-4 lg:p-5">
203 {importError && (
204 <div className="mb-3 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
205 {importError}
206 </div>
207 )}
208 <ChartDataEditor
209 key={`${chartType}-${importVersion}`}
210 data={localData}
211 chartType={chartType}
212 onDataChange={handleDataChange}
213 isComposedChart={isComposedChart}
214 seriesChartTypes={localSeriesChartTypes}
215 onSeriesChartTypesChange={handleSeriesChartTypesChange}
216 />
217 </section>
218 </div>
219
220 <CredenzaFooter className="shrink-0 border-t px-4 py-3 sm:px-5 sm:py-4">
221 <Button variant="outline" onClick={handleCancel}>
222 Cancel
223 </Button>
224 <Button onClick={handleSave}>Save Changes</Button>
225 </CredenzaFooter>
226 </CredenzaContent>
227 </Credenza>
228 );
229 }
230
231 // Re-export types for backward compatibility
232 export type {
233 ChartDataMode,
234 ChartDataType,
235 SeriesChartType,
236 } from "./chart-data-editor";
237
237 lines Plain Text