feat: insert evidence directives from the TechLog asset picker
Adds an Asset Picker and upload dialog to the CASE editor so authors can insert `:::evidence` directives that reference backend assets, and opens projectWorkingCopy's two evidence gates so Instant Preview accepts a key backed by a freshly loaded READY asset instead of only the one hardcoded legacy key. The editor screen now owns the loaded Asset list so the Picker, the upload dialog, and Instant Preview all read the same array, and a freshly uploaded asset appears in the preview without a refetch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e2c1d076f4
commit
54d9bf9120
@@ -0,0 +1,84 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { Asset } from "../../../contracts/studio/contract.ts";
|
||||
import type { StudioAssetGateway } from "../../../application/ports/studio-asset-gateway.ts";
|
||||
|
||||
/**
|
||||
* A directive attribute value is delimited by double quotes (see
|
||||
* `parse-case-content.ts`'s `attributesOf`), so a value containing one would
|
||||
* corrupt the directive -- and a raw newline would spill the directive across
|
||||
* lines the block-directive parser does not expect inside an attribute. Both
|
||||
* are stripped/folded rather than escaped: there is no escape syntax the
|
||||
* parser accepts inside a directive attribute.
|
||||
*/
|
||||
function attributeValue(raw: string): string {
|
||||
return raw.replaceAll('"', "").replace(/\s+/gu, " ").trim();
|
||||
}
|
||||
|
||||
export function buildEvidenceDirective(
|
||||
input: Readonly<{ assetKey: string; alt: string; caption: string; zoom: boolean }>,
|
||||
): string {
|
||||
const key = attributeValue(input.assetKey);
|
||||
const alt = attributeValue(input.alt);
|
||||
const caption = attributeValue(input.caption);
|
||||
return `:::evidence key="${key}" alt="${alt}" caption="${caption}" zoom="${input.zoom}"\n:::`;
|
||||
}
|
||||
|
||||
export function AssetPicker({
|
||||
gateway,
|
||||
onInsert,
|
||||
onLoaded,
|
||||
}: Readonly<{
|
||||
gateway: StudioAssetGateway;
|
||||
onInsert: (directive: string) => void;
|
||||
/**
|
||||
* Optional so Task 10 Step 1's original test (which renders `AssetPicker`
|
||||
* without it) keeps passing. When supplied, the editor screen uses this to
|
||||
* own the same Asset list the Instant Preview resolver reads -- so a
|
||||
* directive this Picker just inserted renders immediately instead of as a
|
||||
* placeholder.
|
||||
*/
|
||||
onLoaded?: (assets: readonly Asset[]) => void;
|
||||
}>) {
|
||||
const [assets, setAssets] = useState<readonly Asset[]>([]);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
gateway
|
||||
.listAssets({ managementStatus: "READY", limit: 50 }, { signal: controller.signal })
|
||||
.then((page) => {
|
||||
setAssets(page.items);
|
||||
onLoaded?.(page.items);
|
||||
})
|
||||
.catch(() => setFailed(true));
|
||||
return () => controller.abort();
|
||||
}, [gateway, onLoaded]);
|
||||
|
||||
// READY만 삽입 후보다. 서버 필터를 신뢰하되 방어적으로 한 번 더 거른다.
|
||||
const selectable = assets.filter((asset) => asset.managementStatus === "READY");
|
||||
|
||||
if (failed) return <p className="studio-error">Asset 목록을 불러오지 못했습니다.</p>;
|
||||
|
||||
if (selectable.length === 0) {
|
||||
return <p className="studio-asset-picker-empty">삽입할 수 있는 Asset이 없습니다. 먼저 업로드하세요.</p>;
|
||||
}
|
||||
|
||||
return <div className="asset-picker">
|
||||
<ul>
|
||||
{selectable.map((asset) => <li key={asset.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onInsert(buildEvidenceDirective({
|
||||
assetKey: asset.assetKey,
|
||||
alt: asset.decorative ? "" : (asset.altText ?? ""),
|
||||
caption: "",
|
||||
zoom: asset.kind === "DIAGRAM",
|
||||
}))}
|
||||
>
|
||||
{asset.assetKey}
|
||||
</button>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user