fix(cv): expose editor errors and controls
CI and Deploy / test (pull_request) Failing after 1m29s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-09 21:06:11 +02:00
parent a5b74e0ab8
commit 20433491db
2 changed files with 58 additions and 12 deletions
+16 -9
View File
@@ -60,6 +60,8 @@ export default function CvBuilderEditor() {
const [html, setHtml] = useState("");
const [zoom, setZoom] = useState(0.62);
const [previewing, setPreviewing] = useState(false);
const [previewError, setPreviewError] = useState(false);
const [previewRevision, setPreviewRevision] = useState(0);
const [pages, setPages] = useState(1);
const [page, setPage] = useState(1);
const [saveState, setSaveState] = useState<SaveState>("idle");
@@ -119,8 +121,9 @@ export default function CvBuilderEditor() {
try {
const render = await cvBuilderApi.previewSettings(settings);
setHtml(render.html);
setPreviewError(false);
} catch {
/* best-effort; keep last good render */
setPreviewError(true);
} finally {
setPreviewing(false);
}
@@ -128,7 +131,7 @@ export default function CvBuilderEditor() {
return () => {
if (previewTimer.current) clearTimeout(previewTimer.current);
};
}, [settings]);
}, [settings, previewRevision]);
const performSave = useCallback(async (next: CvVariantSettings, nextName: string, revision: number) => {
setSaveState("saving");
@@ -315,6 +318,8 @@ export default function CvBuilderEditor() {
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1, px: 1, flexWrap: "wrap" }}>
<Typography variant="caption" sx={{ fontWeight: 700 }}>Live preview</Typography>
{previewing && <Chip size="small" label="updating…" variant="outlined" />}
{previewError && <Chip size="small" label="Preview unavailable" color="error" variant="outlined" />}
{previewError && <Button size="small" onClick={() => setPreviewRevision((revision) => revision + 1)}>Retry preview</Button>}
<Box sx={{ flex: 1 }} />
{pages > 1 && (
<Stack direction="row" alignItems="center" spacing={0.5}>
@@ -538,6 +543,7 @@ function SectionRow({
}) {
const [expanded, setExpanded] = useState(false);
const editable = outlineSection?.kind === "entries" && (outlineSection?.entries.length ?? 0) > 0;
const sectionLabel = row.title ?? SECTION_LABELS[row.key] ?? row.key;
return (
<Paper
@@ -552,19 +558,19 @@ function SectionRow({
<Stack direction="row" alignItems="center" gap={0.5}>
<DragIndicatorIcon sx={{ fontSize: 18, color: "text.disabled" }} aria-hidden />
<Stack>
<IconButton size="small" aria-label="Move section up" disabled={index === 0} onClick={() => onMove(-1)}><ArrowUpwardIcon sx={{ fontSize: 15 }} /></IconButton>
<IconButton size="small" aria-label="Move section down" disabled={index === total - 1} onClick={() => onMove(1)}><ArrowDownwardIcon sx={{ fontSize: 15 }} /></IconButton>
<IconButton size="small" aria-label={`Move ${sectionLabel} section up`} disabled={index === 0} onClick={() => onMove(-1)}><ArrowUpwardIcon sx={{ fontSize: 15 }} /></IconButton>
<IconButton size="small" aria-label={`Move ${sectionLabel} section down`} disabled={index === total - 1} onClick={() => onMove(1)}><ArrowDownwardIcon sx={{ fontSize: 15 }} /></IconButton>
</Stack>
<TextField variant="standard" fullWidth value={row.title ?? SECTION_LABELS[row.key] ?? row.key}
onChange={(e) => onPatch({ title: e.target.value })}
slotProps={{ input: { disableUnderline: true }, htmlInput: { "aria-label": `Section name for ${row.key}` } }} />
{editable && (
<IconButton size="small" aria-label="Edit entries" onClick={() => setExpanded((e) => !e)}
<IconButton size="small" aria-label={`${expanded ? "Collapse" : "Expand"} ${sectionLabel} entries`} aria-expanded={expanded} onClick={() => setExpanded((e) => !e)}
sx={{ transform: expanded ? "rotate(180deg)" : "none", transition: "transform 150ms" }}>
<ExpandMoreIcon fontSize="small" />
</IconButton>
)}
<IconButton size="small" aria-label={row.hidden ? "Show section" : "Hide section"} onClick={() => onPatch({ hidden: !row.hidden })}>
<IconButton size="small" aria-label={`${row.hidden ? "Show" : "Hide"} ${sectionLabel} section`} onClick={() => onPatch({ hidden: !row.hidden })}>
{row.hidden ? <VisibilityOffIcon fontSize="small" /> : <VisibilityIcon fontSize="small" />}
</IconButton>
</Stack>
@@ -614,6 +620,7 @@ function EntryEditor({ section, row, settings, onPatch, onUpdateSettings }: {
if (!entry) return null;
const ov = settings.overrides[key] ?? {};
const hidden = !!ov.hidden;
const entryLabel = ov.title || entry.title || "Untitled";
return (
<Paper key={key} variant="outlined" {...drag.getItemProps(i)}
sx={{
@@ -624,9 +631,9 @@ function EntryEditor({ section, row, settings, onPatch, onUpdateSettings }: {
<Stack direction="row" alignItems="center" gap={0.5}>
<DragIndicatorIcon sx={{ fontSize: 16, color: "text.disabled" }} aria-hidden />
<Typography variant="body2" sx={{ fontWeight: 700, flex: 1 }}>{ov.title || entry.title || "Untitled"}</Typography>
<IconButton size="small" aria-label="Move entry up" disabled={i === 0} onClick={() => onPatch({ itemOrder: moveItem(orderedKeys, i, i - 1) })}><ArrowUpwardIcon sx={{ fontSize: 14 }} /></IconButton>
<IconButton size="small" aria-label="Move entry down" disabled={i === orderedKeys.length - 1} onClick={() => onPatch({ itemOrder: moveItem(orderedKeys, i, i + 1) })}><ArrowDownwardIcon sx={{ fontSize: 14 }} /></IconButton>
<IconButton size="small" aria-label={hidden ? "Show entry" : "Hide entry"} onClick={() => setOverride(key, { hidden: !hidden })}>
<IconButton size="small" aria-label={`Move ${entryLabel} entry up`} disabled={i === 0} onClick={() => onPatch({ itemOrder: moveItem(orderedKeys, i, i - 1) })}><ArrowUpwardIcon sx={{ fontSize: 14 }} /></IconButton>
<IconButton size="small" aria-label={`Move ${entryLabel} entry down`} disabled={i === orderedKeys.length - 1} onClick={() => onPatch({ itemOrder: moveItem(orderedKeys, i, i + 1) })}><ArrowDownwardIcon sx={{ fontSize: 14 }} /></IconButton>
<IconButton size="small" aria-label={`${hidden ? "Show" : "Hide"} ${entryLabel} entry`} onClick={() => setOverride(key, { hidden: !hidden })}>
{hidden ? <VisibilityOffIcon sx={{ fontSize: 16 }} /> : <VisibilityIcon sx={{ fontSize: 16 }} />}
</IconButton>
</Stack>