Spaces:
Sleeping
Sleeping
| import { useEffect, useState } from "react"; | |
| import { FileText, Trash2, BarChart3, Database, AlertCircle } from "lucide-react"; | |
| import type { EmbedStore } from "../editor/embeds/embed-store"; | |
| import type { EmbedDataFileMeta } from "../editor/embeds/embed-data-store"; | |
| import { UploadZone } from "./UploadZone"; | |
| import { formatBytes } from "../utils/data-files"; | |
| import type { UploadResult } from "../hooks/useEmbedData"; | |
| interface FilesSidebarProps { | |
| embedStore: EmbedStore | null; | |
| currentSrc: string; | |
| dataFiles: EmbedDataFileMeta[]; | |
| selectedDataFile: string | null; | |
| /** | |
| * Switch the embed studio to another chart by filename. Optional - | |
| * when omitted, the chart list renders as read-only labels (the | |
| * previous behaviour). | |
| */ | |
| onSelectChart?: (name: string) => void; | |
| onSelectDataFile: (name: string | null) => void; | |
| onUploadFiles: (files: FileList | File[]) => Promise<UploadResult[]>; | |
| onRemoveDataFile: (name: string) => void; | |
| } | |
| export function FilesSidebar({ | |
| embedStore, | |
| currentSrc, | |
| dataFiles, | |
| selectedDataFile, | |
| onSelectChart, | |
| onSelectDataFile, | |
| onUploadFiles, | |
| onRemoveDataFile, | |
| }: FilesSidebarProps) { | |
| const [charts, setCharts] = useState<string[]>(() => | |
| embedStore ? embedStore.keys() : [], | |
| ); | |
| const [uploadError, setUploadError] = useState<string | null>(null); | |
| useEffect(() => { | |
| if (!embedStore) return; | |
| setCharts(embedStore.keys()); | |
| return embedStore.observe(() => setCharts(embedStore.keys())); | |
| }, [embedStore]); | |
| const handleUpload = async (files: FileList | File[]) => { | |
| setUploadError(null); | |
| const results = await onUploadFiles(files); | |
| const firstError = results.find((r) => !r.ok); | |
| if (firstError?.error) setUploadError(firstError.error); | |
| const firstOk = results.find((r) => r.ok && r.name); | |
| if (firstOk?.name) onSelectDataFile(firstOk.name); | |
| }; | |
| return ( | |
| <div className="es-files"> | |
| <section className="es-files__section"> | |
| <header className="es-files__section-title"> | |
| <BarChart3 size={13} /> | |
| <span>Charts</span> | |
| <span className="es-files__count">{charts.length}</span> | |
| </header> | |
| <ul className="es-files__list"> | |
| {charts.length === 0 && ( | |
| <li className="es-files__empty">No charts yet</li> | |
| )} | |
| {charts.map((name) => { | |
| const isCurrent = name === currentSrc; | |
| const className = `es-files__item ${isCurrent ? "es-files__item--current" : ""}`; | |
| const inner = ( | |
| <> | |
| <FileText size={12} /> | |
| <span className="es-files__name">{name}</span> | |
| {isCurrent && ( | |
| <span className="es-files__badge">editing</span> | |
| )} | |
| </> | |
| ); | |
| if (!onSelectChart || isCurrent) { | |
| return ( | |
| <li | |
| key={name} | |
| className={className} | |
| title={isCurrent ? "Current chart" : name} | |
| > | |
| {inner} | |
| </li> | |
| ); | |
| } | |
| return ( | |
| <li key={name} className={className}> | |
| <button | |
| type="button" | |
| className="es-files__item-main" | |
| onClick={() => onSelectChart(name)} | |
| title={`Switch to ${name}`} | |
| > | |
| {inner} | |
| </button> | |
| </li> | |
| ); | |
| })} | |
| </ul> | |
| </section> | |
| <section className="es-files__section"> | |
| <header className="es-files__section-title"> | |
| <Database size={13} /> | |
| <span>Data</span> | |
| <span className="es-files__count">{dataFiles.length}</span> | |
| </header> | |
| <ul className="es-files__list"> | |
| {dataFiles.length === 0 && ( | |
| <li className="es-files__empty">No data uploaded yet</li> | |
| )} | |
| {dataFiles.map((file) => { | |
| const isSelected = file.name === selectedDataFile; | |
| return ( | |
| <li | |
| key={file.name} | |
| className={`es-files__item es-files__item--data ${isSelected ? "es-files__item--current" : ""}`} | |
| > | |
| <button | |
| type="button" | |
| className="es-files__item-main" | |
| onClick={() => onSelectDataFile(isSelected ? null : file.name)} | |
| title={file.name} | |
| > | |
| <FileText size={12} /> | |
| <span className="es-files__name">{file.name}</span> | |
| <span className="es-files__size">{formatBytes(file.size)}</span> | |
| </button> | |
| <button | |
| type="button" | |
| className="es-files__item-remove" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| if (isSelected) onSelectDataFile(null); | |
| onRemoveDataFile(file.name); | |
| }} | |
| aria-label={`Remove ${file.name}`} | |
| title="Remove file" | |
| > | |
| <Trash2 size={12} /> | |
| </button> | |
| </li> | |
| ); | |
| })} | |
| </ul> | |
| <UploadZone onFiles={handleUpload} compact={dataFiles.length > 0} /> | |
| {uploadError && ( | |
| <div className="es-files__error"> | |
| <AlertCircle size={12} /> | |
| <span>{uploadError}</span> | |
| </div> | |
| )} | |
| </section> | |
| </div> | |
| ); | |
| } | |