11 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick | 260420-vk0 | execute | 1 |
|
true |
|
Purpose: Close remaining UAT gaps so admin pages are fully functional. Output: Working image fetch-from-URL with preview, working tag routing, proper error handling for duplicate tags, polished tag form.
<execution_context> @.planning/quick/260420-vk0-fix-uat-issues-image-fetch-from-url-imag/260420-vk0-PLAN.md </execution_context>
@CLAUDE.md @src/server/routes/images.ts @src/client/routes/admin/items/$itemId.tsx @src/server/routes/admin-tags.ts @src/client/routes/admin/tags/index.tsx @src/client/routes/admin/tags/$tagId.tsx @src/client/components/ImageUpload.tsx From src/server/services/image.service.ts: ```typescript interface FetchImageResult { filename: string; sourceUrl: string; dominantColor: string | null; } export async function fetchImageFromUrl(url: string): Promise; ```From src/server/services/storage.service.ts:
export async function getImageUrl(filename: string): Promise<string>;
From src/client/components/ImageUpload.tsx:
interface ImageUploadProps {
value: string | null;
imageUrl?: string | null;
dominantColor?: string | null;
initialCrop?: { zoom: number; x: number; y: number } | null;
onChange: (filename: string | null, dominantColor?: string | null) => void;
onCropChange?: (crop: { zoom: number; x: number; y: number }) => void;
}
Client side (src/client/routes/admin/items/$itemId.tsx):
In handleFetchFromUrl (line 159), update the type annotation to include presignedUrl: string and dominantColor: string | null in the response type. After a successful fetch, also update imageUrl and dominantColor in form state:
const result = await apiPost<{ filename: string; sourceUrl: string; presignedUrl: string; dominantColor: string | null }>(
"/api/images/from-url",
{ url: fetchUrl.trim() },
);
setForm((prev) => ({
...prev,
imageFilename: result.filename,
imageUrl: result.presignedUrl,
dominantColor: result.dominantColor ?? "",
imageSourceUrl: fetchUrl.trim(),
}));
This ensures the ImageUpload component receives the presigned URL via imageUrl prop and displays the fetched image immediately. With the image displaying, the crop button becomes visible and functional (cropping already works via ImageCropEditor — the issue was just that no image was shown to crop).
cd /home/jlmak/Projects/jlmak/GearBox && bun run lint
After fetching an image from URL on the admin item edit page, the image preview displays immediately and the crop button is visible/functional.
-
First, regenerate the route tree: run
bunx tsr generate(TanStack Router CLI) to ensure the route tree is fresh. -
If that doesn't resolve it, check if the
createFileRoutepath string in$tagId.tsxmatches exactly what TanStack Router expects. Currently it'screateFileRoute("/admin/tags/$tagId")— verify this matches the route tree's registered path pattern. If the route tree expects a different format (e.g., relative path), update accordingly. -
If the route tree is correct but the page still shows the list, the issue may be that TanStack Router is matching the index route instead of the param route. The fix (same as was done for items in commit
31a9e3c) may involve ensuring the route file path conventions match TanStack Router's expectations for the directory-based approach. Check if items has any difference in file structure or route declaration that makes it work while tags doesn't. -
After investigation, ensure navigating to
/admin/tags/4rendersAdminTagEditPage(not the tag list). cd /home/jlmak/Projects/jlmak/GearBox && bunx tsr generate && bun run lint Navigating to /admin/tags/$tagId renders the tag edit form, not the tag list page.
Frontend (src/client/routes/admin/tags/index.tsx):
- In
handleCreate, improve error handling to show the server's error message:
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
if (!newName.trim()) return;
setCreateError(null);
try {
await createMutation.mutateAsync({
name: newName.trim(),
parentId: newParentId,
});
setNewName("");
setNewParentId(null);
} catch (err: any) {
const message = err?.response?.error || err?.message || "Failed to create tag";
setCreateError(message);
}
}
Note: Check how apiPost (or the mutation hook) propagates error response bodies. If the hook uses React Query's mutateAsync, the error may be thrown as-is from the API client. Look at how useCreateAdminTag is implemented and ensure the error field from the 409 JSON response is surfaced. If the api client throws a generic Error, you may need to parse the response body in the hook or catch block.
- Polish the inline tag creation form. Replace the plain
flex items-center gap-3row with a more visually consistent card-style form:
{/* Quick-add form */}
<div className="mb-6 rounded-xl border border-gray-100 bg-white p-4">
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-3">Add Tag</p>
<form onSubmit={handleCreate} className="flex items-end gap-3">
<div className="flex-1">
<label className="block text-xs text-gray-500 mb-1">Name</label>
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="e.g. Bikepacking"
className={inputClass}
/>
</div>
<div className="w-48">
<label className="block text-xs text-gray-500 mb-1">Parent</label>
<select
value={newParentId ?? ""}
onChange={(e) => setNewParentId(e.target.value ? Number(e.target.value) : null)}
className={`${inputClass} appearance-none bg-white`}
>
<option value="">No parent (top-level)</option>
{data?.map((t) => (
<option key={t.id} value={t.id}>{t.name}</option>
))}
</select>
</div>
<button
type="submit"
disabled={createMutation.isPending || !newName.trim()}
className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium transition-colors disabled:opacity-50 shrink-0"
>
{createMutation.isPending ? "Adding..." : "Add Tag"}
</button>
</form>
{createError && (
<p className="text-sm text-red-500 mt-2">{createError}</p>
)}
</div>
This wraps the form in a card with labels, making it visually consistent with the rest of the admin UI and easier to navigate. cd /home/jlmak/Projects/jlmak/GearBox && bun run lint Creating a duplicate tag shows "A tag named X already exists" error. The inline form has labels, is wrapped in a card, and matches admin UI style.
1. `bun run lint` passes with no errors 2. Start dev server (`bun run dev`) and verify: - Navigate to /admin/items/{id}, use "Fetch from URL" with a valid image URL — preview appears - Click crop button on the fetched image — crop editor opens - Navigate to /admin/tags/{id} — edit form renders (not the list) - On /admin/tags, try creating a tag with a duplicate name — shows friendly error - Inline form has card styling with labels<success_criteria>
- All 5 UAT issues are resolved
- No lint errors introduced
- Image fetch-from-URL shows preview immediately
- Tag routing renders edit page at /admin/tags/$tagId
- Duplicate tag creation shows 409 error message (not 500)
- Tag form is visually polished with labels and card wrapper </success_criteria>