tfrere's picture
tfrere HF Staff
feat(embed-studio): clickable chart list to switch files
79008fb
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>
);
}