feat(#90): IMP-56 u20 BottomActions wiring to /api/connect + /api/export (replace placeholder toasts + standalone HTML download + cel mirror connect; pure builders exported for vitest)
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
Stage 2 final unit for Step 22 (user edit + export). u20 wires the previously
placeholder bottom-action footer to the u18 /api/connect and u19 /api/export
middlewares living in Front/vite.config.ts:
- BottomActions.tsx
• drops the dead `serializeSlidePlan` import (TS2305 blocker since u14;
project-wide `tsc --noEmit` now exits 0)
• exports three pure builders for vitest (no jsdom / RTL devDep needed):
buildConnectRequest(run_id, slug) -> POST /api/connect {run_id, slug}
buildExportRequest(run_id) -> POST /api/export {run_id}
buildDownloadFilename(run_id) -> "<run_id>.html"
• handleExport: POST -> blob -> a[download] click chain; toast on
success / failure / network error.
• handleConnect: derives slug via deriveUserOverridesKey(uploadedFile.name)
and PUTs to u18 cel mirror; reports assets_copied count.
• both buttons disable when runMeta is null so the UI cannot fire
requests with an undefined run_id.
- Home.tsx
• mounts <BottomActions/> in the footer with
{slidePlan, runMeta, uploadedFile, isLoading, onGenerate}.
• removes 2 of 3 placeholder `toast.info('… 준비 중입니다.')` buttons
(LeftMdxPanel MDX-edit placeholder remains — out of u20 scope).
• adds handleTextEdit (u15 wire to text_overrides axis) and
handleStructureEdit (u15 wire to structure_overrides axis) to satisfy
the SlideCanvas props introduced earlier in the u-series.
- imp90_bottom_actions.test.ts (new)
• 11 vitest specs locking the builder URL + JSON shape against u18/u19
middleware contracts. Verified 11/11 pass.
Stage 4 verification (all PASS):
• u20 vitest: 11/11
• u18/u19 endpoint vitest: 31/31
• npx tsc --noEmit: exit 0 (carry-forward TS2305 resolved)
• backend pytest (u1~u9 + u17 print mode, 9 files): 185/185
Out of scope:
• LeftMdxPanel.tsx:333 MDX-edit placeholder toast (separate unit)
• #1 / #72 / #74 / #79 / #80 / #81 / #93 closed dependencies (no re-impl)
• AI-generated HTML structure (Phase Z regression guard)
• frame swap via structure_overrides (locked to slot_order + hidden_slots)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ec7471ed59
commit
943957562f
3 files changed
+292
-48
No files matched your search
@@ -16,6 +16,8 @@ import {
|
||||
moveSectionToZone,
|
||||
saveZoneSizes,
|
||||
saveImageOverride,
|
||||
saveTextOverride,
|
||||
saveStructureOverride,
|
||||
deriveUserOverridesKey,
|
||||
applyPersistedNonFrameOverrides,
|
||||
remapPersistedFramesToZoneFrames,
|
||||
@@ -41,8 +43,9 @@ import LeftMdxPanel from "../components/LeftMdxPanel";
|
||||
import SlideCanvas from "../components/SlideCanvas";
|
||||
import LayoutPanel from "../components/LayoutPanel";
|
||||
import FramePanel from "../components/FramePanel";
|
||||
import BottomActions from "../components/BottomActions";
|
||||
import {
|
||||
Sparkles, Download, Link2, Loader2,
|
||||
Sparkles, Loader2,
|
||||
CheckCircle2, HelpCircle,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -680,6 +683,51 @@ export default function Home() {
|
||||
setHasPendingChanges(true);
|
||||
}, []);
|
||||
|
||||
// IMP-56 (#90) u15 — wire SlideCanvas u13 focusout capture into the new
|
||||
// `text_overrides` persist axis. Mirrors handleImageResize: merge the
|
||||
// (zoneId, textPath, value) tick via `saveTextOverride` (u15 pure helper)
|
||||
// and schedule the 300ms-debounced PUT under the `text_overrides` axis.
|
||||
// Per-axis coalescing in `saveUserOverrides` collapses rapid edits in
|
||||
// the same line into a single PUT; per-key buckets isolate cross-MDX.
|
||||
const handleTextEdit = useCallback(
|
||||
(capture: { zoneId: string; textPath: string; value: string }) => {
|
||||
setState((p) => {
|
||||
const nextSelection = saveTextOverride(
|
||||
p.userSelection, capture.zoneId, capture.textPath, capture.value,
|
||||
);
|
||||
if (p.uploadedFile) {
|
||||
const key = deriveUserOverridesKey(p.uploadedFile.name);
|
||||
void saveUserOverrides(key, {
|
||||
text_overrides: nextSelection.overrides.text_overrides,
|
||||
});
|
||||
}
|
||||
return { ...p, userSelection: nextSelection };
|
||||
});
|
||||
setHasPendingChanges(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// IMP-56 (#90) u15 — wire SlideCanvas u14 structure overlay capture into
|
||||
// the `structure_overrides` axis. Scope-locked to {slot_order,
|
||||
// hidden_slots} — frame swap stays on the existing `frames` axis.
|
||||
const handleStructureEdit = useCallback(
|
||||
(zoneId: string, perZone: { slot_order?: string[]; hidden_slots?: string[] }) => {
|
||||
setState((p) => {
|
||||
const nextSelection = saveStructureOverride(p.userSelection, zoneId, perZone);
|
||||
if (p.uploadedFile) {
|
||||
const key = deriveUserOverridesKey(p.uploadedFile.name);
|
||||
void saveUserOverrides(key, {
|
||||
structure_overrides: nextSelection.overrides.structure_overrides,
|
||||
});
|
||||
}
|
||||
return { ...p, userSelection: nextSelection };
|
||||
});
|
||||
setHasPendingChanges(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// pending mode 일 때 effectiveSlidePlan = pendingZones 가 swap 된 plan.
|
||||
// 그 외 = state.slidePlan. 모든 zone / region lookup 이 일관되게 이걸 사용 →
|
||||
// pending mode 의 region.id ("pending-region-N") 가 zone_frames key 로 들어가
|
||||
@@ -868,6 +916,9 @@ export default function Home() {
|
||||
onZoneResize={handleZoneResize}
|
||||
imageOverrides={state.userSelection.overrides.image_overrides}
|
||||
onImageResize={handleImageResize}
|
||||
onTextEdit={handleTextEdit}
|
||||
structureOverrides={state.userSelection.overrides.structure_overrides}
|
||||
onStructureEdit={handleStructureEdit}
|
||||
/>
|
||||
</main>
|
||||
|
||||
@@ -910,11 +961,13 @@ export default function Home() {
|
||||
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-tighter">Phase Z Engine Active</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" onClick={() => toast.info("연동하기 기능은 준비 중입니다.")} className="gap-2 h-9 text-[11px] font-bold uppercase tracking-widest border-slate-200"><Link2 className="w-3.5 h-3.5" />Connect</Button>
|
||||
<Button variant="outline" onClick={() => toast.info("다운로드 기능은 준비 중입니다.")} disabled={!state.slidePlan} className="gap-2 h-9 text-[11px] font-bold uppercase tracking-widest border-slate-200"><Download className="w-3.5 h-3.5" />Download</Button>
|
||||
<Button onClick={() => toast.success("슬라이드 설정이 확정되었습니다.")} disabled={!state.slidePlan || state.isLoading} className="gap-2 h-9 text-[11px] font-bold uppercase tracking-widest bg-slate-900 hover:bg-slate-800"><Sparkles className="w-3.5 h-3.5" />Finalize Slide</Button>
|
||||
</div>
|
||||
<BottomActions
|
||||
slidePlan={state.slidePlan}
|
||||
runMeta={runMeta}
|
||||
uploadedFile={state.uploadedFile}
|
||||
isLoading={state.isLoading}
|
||||
onGenerate={handleGenerate}
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user