516 lines
26 KiB
Markdown
516 lines
26 KiB
Markdown
# Phase 23: Manual Entry Fallback - Research
|
|
|
|
**Researched:** 2026-04-06
|
|
**Domain:** React UI — inline form view state within CatalogSearchOverlay
|
|
**Confidence:** HIGH
|
|
|
|
## Summary
|
|
|
|
Phase 23 is a pure frontend phase. The goal is to give users a way to add items that do not exist in the global catalog by surfacing a manual entry form inline inside the CatalogSearchOverlay. The backend is already fully capable — `POST /api/items` without a `globalItemId` creates a standalone collection item, and `createItemSchema` accepts the call without that field. Nothing on the server needs to change.
|
|
|
|
The implementation has two distinct layers. First, the entry points: an "Add Manually" link in the EmptyState component and a persistent but subtle link below the results area. Second, the inline form view: a new `ManualEntryForm` component rendered inside `CatalogSearchOverlay` when `manualEntryMode` state is true. The form reuses established patterns from `ItemForm` and `AddToCollectionModal` but is lighter and more focused. After a successful save, the results area is replaced by a success card with a non-functional "Submit to Catalog?" button and "Add Another" / "Done" actions.
|
|
|
|
There is no new route, no new API endpoint, and no Zustand store change required. All view-switching state is local to `CatalogSearchOverlay` via `useState`. This is the simplest possible scope for the phase.
|
|
|
|
**Primary recommendation:** Implement the entire phase as local state inside CatalogSearchOverlay — `manualEntryMode: boolean` and `savedItemName: string | null` — with a new sibling component file `ManualEntryForm.tsx`.
|
|
|
|
---
|
|
|
|
<user_constraints>
|
|
## User Constraints (from CONTEXT.md)
|
|
|
|
### Locked Decisions
|
|
|
|
- **D-01:** "Add Manually" link appears in the catalog search empty state (when no results match) AND as a subtle persistent link below search results.
|
|
- **D-02:** Link text is "Add Manually" or "Can't find it? Add manually" — context-sensitive based on whether a search query exists.
|
|
- **D-03:** Manual entry form replaces the search results area inline within CatalogSearchOverlay. No navigation away or additional modal.
|
|
- **D-04:** Back arrow at the top returns from the manual form to search results.
|
|
- **D-05:** The form is a dedicated ManualEntryForm component rendered inside CatalogSearchOverlay when `manualEntryMode` state is active.
|
|
- **D-06:** Required: name. Optional: category (via CategoryPicker), weight (grams), price (cents), notes, purchase price, image upload, product link.
|
|
- **D-07:** Reuse field patterns from ItemForm (CategoryPicker, weight/price inputs) but keep the form focused and compact — not the full 315-line ItemForm.
|
|
- **D-08:** Submit calls `POST /api/items` without `globalItemId` — creates a standalone collection item.
|
|
- **D-09:** After successful save, show an inline success card: "Added [item name] to collection" with a "Submit to Catalog?" button.
|
|
- **D-10:** "Submit to Catalog?" button is non-functional — shows a toast "Coming soon" when clicked. No backend action.
|
|
- **D-11:** Below the prompt, show "Add Another" (returns to search) and "Done" (closes overlay) buttons.
|
|
|
|
### Claude's Discretion
|
|
|
|
- Exact form layout proportions and field ordering
|
|
- Whether weight/price fields use formatted inputs or plain number inputs
|
|
- Animation transitions between search results and manual entry form
|
|
- Success card visual styling
|
|
- Whether the search query auto-populates the item name field when entering manual mode
|
|
|
|
### Deferred Ideas (OUT OF SCOPE)
|
|
|
|
- Actual catalog submission backend (admin review, convert to global item) — future phase
|
|
- Bulk manual entry — future phase
|
|
- Image search / URL paste to auto-populate item details — future phase
|
|
</user_constraints>
|
|
|
|
---
|
|
|
|
<phase_requirements>
|
|
## Phase Requirements
|
|
|
|
| ID | Description | Research Support |
|
|
|----|-------------|------------------|
|
|
| CATFLOW-07 | Manual entry fallback when item not in catalog | Implemented via `manualEntryMode` local state in CatalogSearchOverlay + ManualEntryForm component; `useCreateItem()` hook + `POST /api/items` without `globalItemId` already supported |
|
|
| CATFLOW-08 | Non-functional "Submit to catalog?" prompt shown after manual save | Implemented as an inline success card within CatalogSearchOverlay after mutation success; "Submit to Catalog?" button calls `toast("Coming soon")` |
|
|
</phase_requirements>
|
|
|
|
---
|
|
|
|
## Standard Stack
|
|
|
|
### Core
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| React | 19 (project) | Component rendering and local state | Project stack |
|
|
| TanStack React Query | project version | `useCreateItem()` mutation | Established data layer |
|
|
| Framer Motion | project version | Animation between search/form views | Already used in CatalogSearchOverlay |
|
|
| sonner `toast` | project version | Success/error/coming-soon notifications | Established project pattern |
|
|
| Zustand | project version | Overlay open/close state (no change needed) | Established UI state pattern |
|
|
|
|
### Supporting
|
|
| Library | Version | Purpose | When to Use |
|
|
|---------|---------|---------|-------------|
|
|
| CategoryPicker | internal | Category selection with search + inline create | Required by D-06 |
|
|
| ImageUpload | internal | Image upload with preview | Required by D-06 |
|
|
|
|
### Alternatives Considered
|
|
| Instead of | Could Use | Tradeoff |
|
|
|------------|-----------|----------|
|
|
| Local `useState` for mode switching | Zustand UIStore | Local state is simpler; no cross-component coordination needed; CONTEXT.md D-05 implies local state |
|
|
|
|
**Installation:** No new packages needed.
|
|
|
|
## Architecture Patterns
|
|
|
|
### Recommended Project Structure
|
|
|
|
No new directories. Two new files:
|
|
|
|
```
|
|
src/client/components/
|
|
├── CatalogSearchOverlay.tsx # Modified — add mode state, entry points, conditional rendering
|
|
└── ManualEntryForm.tsx # New — standalone form component for manual item entry
|
|
```
|
|
|
|
### Pattern 1: Inline View Switching via Local State
|
|
|
|
**What:** CatalogSearchOverlay already uses local `useState` for `viewMode`, `filterOpen`, `searchInput`, etc. The same pattern extends naturally to `manualEntryMode`.
|
|
|
|
**When to use:** When an overlay needs to transition between views without navigating or spawning a new modal layer.
|
|
|
|
**Example:**
|
|
```typescript
|
|
// Inside CatalogSearchOverlay
|
|
const [manualEntryMode, setManualEntryMode] = useState(false);
|
|
const [savedItemName, setSavedItemName] = useState<string | null>(null);
|
|
|
|
// Reset when overlay closes (add to existing reset effect)
|
|
useEffect(() => {
|
|
if (!catalogSearchOpen) {
|
|
setManualEntryMode(false);
|
|
setSavedItemName(null);
|
|
// ... existing resets
|
|
}
|
|
}, [catalogSearchOpen]);
|
|
|
|
// Render switch
|
|
{manualEntryMode
|
|
? savedItemName
|
|
? <SuccessCard itemName={savedItemName} onAddAnother={...} onDone={...} />
|
|
: <ManualEntryForm initialName={searchInput} onSuccess={...} onBack={...} />
|
|
: <ResultsArea ... />
|
|
}
|
|
```
|
|
|
|
**Source:** CatalogSearchOverlay.tsx existing local state pattern (verified by reading the file).
|
|
|
|
### Pattern 2: Compact Form Adapted from ItemForm
|
|
|
|
**What:** ManualEntryForm mirrors ItemForm's field patterns (validation, CategoryPicker, weight/price conversion) but is a lighter self-contained component — not tied to UIStore.
|
|
|
|
**Key adaptation from ItemForm:**
|
|
- No `mode: "add" | "edit"` — always add-only
|
|
- No `useItems()` call — no need for pre-fill
|
|
- No delete button
|
|
- Accepts `initialName` prop (auto-populated from search query per CONTEXT.md specifics)
|
|
- Accepts `onSuccess(itemName: string)` and `onBack()` callbacks
|
|
- `purchasePriceCents` field (from AddToCollectionModal) added alongside the regular `priceCents` field
|
|
|
|
**Example component signature:**
|
|
```typescript
|
|
interface ManualEntryFormProps {
|
|
initialName?: string;
|
|
onSuccess: (itemName: string) => void;
|
|
onBack: () => void;
|
|
}
|
|
```
|
|
|
|
**Source:** Verified by reading `src/client/components/ItemForm.tsx` and `src/client/components/AddToCollectionModal.tsx`.
|
|
|
|
### Pattern 3: useCreateItem without globalItemId
|
|
|
|
**What:** The existing `useCreateItem()` mutation accepts `CreateItem` from `src/shared/types`. The `createItemSchema` in `src/shared/schemas.ts` defines `globalItemId` as `.optional()`, so omitting it is fully valid.
|
|
|
|
**Example:**
|
|
```typescript
|
|
createItem.mutate({
|
|
name: form.name.trim(),
|
|
categoryId: form.categoryId,
|
|
weightGrams: form.weightGrams ? Number(form.weightGrams) : undefined,
|
|
priceCents: form.priceDollars ? Math.round(Number(form.priceDollars) * 100) : undefined,
|
|
notes: form.notes || undefined,
|
|
productUrl: form.productUrl || undefined,
|
|
imageFilename: form.imageFilename ?? undefined,
|
|
purchasePriceCents: form.purchasePrice ? Math.round(Number(form.purchasePrice) * 100) : undefined,
|
|
// globalItemId deliberately omitted
|
|
}, {
|
|
onSuccess: (item) => onSuccess(item.name),
|
|
onError: (err) => setError(err instanceof Error ? err.message : "Failed to save"),
|
|
});
|
|
```
|
|
|
|
**Source:** `src/shared/schemas.ts` line 13 (`globalItemId: z.number().int().positive().optional()`), `src/client/hooks/useItems.ts` lines 62-72.
|
|
|
|
### Pattern 4: "Add Manually" Entry Points
|
|
|
|
**What:** Two placements per D-01 — inside EmptyState and as a persistent link below results.
|
|
|
|
**EmptyState adaptation** (line 667 in CatalogSearchOverlay.tsx):
|
|
```typescript
|
|
function EmptyState({ hasQuery, onAddManually }: {
|
|
hasQuery: boolean;
|
|
onAddManually: () => void;
|
|
}) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-20 px-4">
|
|
{/* existing icon + text */}
|
|
<button
|
|
type="button"
|
|
onClick={onAddManually}
|
|
className="mt-4 text-sm text-blue-600 hover:text-blue-800 underline-offset-2 hover:underline"
|
|
>
|
|
{hasQuery ? "Can't find it? Add manually" : "Add Manually"}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**Persistent link below results:** A `div` rendered after the results grid/list, always visible when not in manual entry mode.
|
|
|
|
### Pattern 5: Toast for Non-functional Button
|
|
|
|
**What:** The "Submit to Catalog?" button (D-10) calls `toast()` from sonner — no backend call.
|
|
|
|
```typescript
|
|
import { toast } from "sonner";
|
|
// ...
|
|
<button type="button" onClick={() => toast("Coming soon — catalog submissions are on the roadmap!")}>
|
|
Submit to Catalog?
|
|
</button>
|
|
```
|
|
|
|
**Source:** `toast` from `sonner` is the established project notification mechanism (confirmed in AddToCollectionModal.tsx and CatalogSearchOverlay usage patterns).
|
|
|
|
### Anti-Patterns to Avoid
|
|
|
|
- **Adding manualEntryMode to Zustand UIStore:** Unnecessary. No other component needs to read this state. CONTEXT.md D-05 says it's local to CatalogSearchOverlay.
|
|
- **Reusing ItemForm directly:** ItemForm is 315 lines, tied to UIStore, and has edit/delete logic not needed here. Adapt the patterns, build a fresh focused component.
|
|
- **Making the form a full-screen overlay on top of the overlay:** The overlay stays. The form replaces the results area content only (D-03).
|
|
- **Emitting a success toast AND showing the success card:** Don't double-notify. The success card IS the success feedback. Skip the `toast.success()` call here (unlike AddToCollectionModal which just closes and toasts).
|
|
|
|
## Don't Hand-Roll
|
|
|
|
| Problem | Don't Build | Use Instead | Why |
|
|
|---------|-------------|-------------|-----|
|
|
| Category selection | Custom select | `CategoryPicker` | Has search, inline create, icon display |
|
|
| Image upload | Custom file input | `ImageUpload` | Handles upload API call, preview, size validation |
|
|
| Price/weight conversion | Custom logic | Adapt from ItemForm | Established pattern: `Math.round(Number(dollars) * 100)` |
|
|
| Toast notifications | Custom UI | `toast` from sonner | Global notification system already wired |
|
|
| Item creation API call | Custom fetch | `useCreateItem()` | Handles query invalidation, error states |
|
|
|
|
**Key insight:** This phase is almost entirely wiring — the building blocks (mutation hook, form components, toast) exist. The work is composing them into a new view state inside CatalogSearchOverlay.
|
|
|
|
## Common Pitfalls
|
|
|
|
### Pitfall 1: Forgetting to Reset manualEntryMode on Overlay Close
|
|
|
|
**What goes wrong:** User closes overlay, reopens it, and the manual entry form is still showing instead of search results.
|
|
|
|
**Why it happens:** The reset `useEffect` (lines 76-87 in CatalogSearchOverlay) clears search/filter state when `catalogSearchOpen` changes to false — but new state like `manualEntryMode` and `savedItemName` won't be in that effect unless explicitly added.
|
|
|
|
**How to avoid:** Add `setManualEntryMode(false)` and `setSavedItemName(null)` to the existing reset effect that triggers on `!catalogSearchOpen`.
|
|
|
|
**Warning signs:** State persists across overlay open/close cycles during testing.
|
|
|
|
### Pitfall 2: "Submit to Catalog?" Button Appearing Functional
|
|
|
|
**What goes wrong:** A loading spinner, disabled state, or API call is added to the "Submit to Catalog?" button, setting a false expectation.
|
|
|
|
**Why it happens:** Developer instinct to make buttons "do something properly."
|
|
|
|
**How to avoid:** The button calls `toast("Coming soon")` and nothing else (D-10). No `isPending`, no `disabled`, no API call.
|
|
|
|
### Pitfall 3: Back Arrow Wiring
|
|
|
|
**What goes wrong:** Back arrow on ManualEntryForm calls `closeCatalogSearch()` instead of returning to search results.
|
|
|
|
**Why it happens:** CatalogSearchOverlay already has a back arrow in the header that closes the overlay. ManualEntryForm needs its own back behavior that sets `manualEntryMode(false)`.
|
|
|
|
**How to avoid:** The ManualEntryForm header renders its own back arrow button that calls the `onBack` prop — `setManualEntryMode(false)`. Alternatively, CatalogSearchOverlay can make its existing header back arrow context-sensitive: when `manualEntryMode`, go back to results; otherwise close overlay.
|
|
|
|
**Warning signs:** Pressing back from the manual form closes the entire overlay instead of returning to search.
|
|
|
|
### Pitfall 4: categoryId Validation
|
|
|
|
**What goes wrong:** `CategoryPicker` is initialized with `value={0}` which doesn't correspond to a real category, causing `categoryId: 0` to be submitted — this will fail the Zod schema (`z.number().int().positive()`).
|
|
|
|
**Why it happens:** Same issue exists in AddToCollectionModal (solved by pre-selecting `categories[0].id` once data loads).
|
|
|
|
**How to avoid:** Follow the AddToCollectionModal pattern — initialize `categoryId` as `null`, pre-select `categories[0].id` once categories data loads, block form submission if `categoryId === null`.
|
|
|
|
### Pitfall 5: Search Query Not Auto-Populating Item Name
|
|
|
|
**What goes wrong:** User types "Ortlieb Gravel Pack" in the search, gets no results, clicks "Add Manually" — the name field is empty. User must retype the name.
|
|
|
|
**Why it happens:** `searchInput` is local state in CatalogSearchOverlay and `ManualEntryForm` doesn't receive it.
|
|
|
|
**How to avoid:** Pass `searchInput` (or `debouncedQuery`) as the `initialName` prop to `ManualEntryForm`. The CONTEXT.md specifics section explicitly calls this out. This is a Claude's Discretion item but the CONTEXT.md says it "should" happen.
|
|
|
|
## Code Examples
|
|
|
|
### ManualEntryForm: Minimal Skeleton
|
|
|
|
```typescript
|
|
// Source: adapted from src/client/components/ItemForm.tsx + AddToCollectionModal.tsx
|
|
import { useEffect, useState } from "react";
|
|
import { ArrowLeft } from "lucide-react";
|
|
import { useCategories } from "../hooks/useCategories";
|
|
import { useCreateItem } from "../hooks/useItems";
|
|
import { CategoryPicker } from "./CategoryPicker";
|
|
import { ImageUpload } from "./ImageUpload";
|
|
|
|
interface ManualEntryFormProps {
|
|
initialName?: string;
|
|
onSuccess: (itemName: string) => void;
|
|
onBack: () => void;
|
|
}
|
|
|
|
export function ManualEntryForm({ initialName, onSuccess, onBack }: ManualEntryFormProps) {
|
|
const { data: categories } = useCategories();
|
|
const createItem = useCreateItem();
|
|
|
|
const [name, setName] = useState(initialName ?? "");
|
|
const [categoryId, setCategoryId] = useState<number | null>(null);
|
|
const [weightGrams, setWeightGrams] = useState("");
|
|
const [priceDollars, setPriceDollars] = useState("");
|
|
const [purchasePrice, setPurchasePrice] = useState("");
|
|
const [notes, setNotes] = useState("");
|
|
const [productUrl, setProductUrl] = useState("");
|
|
const [imageFilename, setImageFilename] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (categories && categories.length > 0 && categoryId === null) {
|
|
setCategoryId(categories[0].id);
|
|
}
|
|
}, [categories, categoryId]);
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!name.trim()) { setError("Name is required"); return; }
|
|
if (categoryId === null) { setError("Select a category"); return; }
|
|
setError(null);
|
|
|
|
createItem.mutate({
|
|
name: name.trim(),
|
|
categoryId,
|
|
weightGrams: weightGrams ? Number(weightGrams) : undefined,
|
|
priceCents: priceDollars ? Math.round(Number(priceDollars) * 100) : undefined,
|
|
purchasePriceCents: purchasePrice ? Math.round(Number(purchasePrice) * 100) : undefined,
|
|
notes: notes || undefined,
|
|
productUrl: productUrl || undefined,
|
|
imageFilename: imageFilename ?? undefined,
|
|
// globalItemId omitted — standalone item
|
|
}, {
|
|
onSuccess: (item) => onSuccess(item.name),
|
|
onError: (err) => setError(err instanceof Error ? err.message : "Failed to save"),
|
|
});
|
|
}
|
|
|
|
// ... render
|
|
}
|
|
```
|
|
|
|
### CatalogSearchOverlay: Mode Switch Wiring
|
|
|
|
```typescript
|
|
// Source: pattern from CatalogSearchOverlay.tsx local state
|
|
const [manualEntryMode, setManualEntryMode] = useState(false);
|
|
const [savedItemName, setSavedItemName] = useState<string | null>(null);
|
|
|
|
// In existing reset effect:
|
|
useEffect(() => {
|
|
if (!catalogSearchOpen) {
|
|
// ...existing resets...
|
|
setManualEntryMode(false);
|
|
setSavedItemName(null);
|
|
}
|
|
}, [catalogSearchOpen]);
|
|
|
|
function handleEnterManualMode() {
|
|
setManualEntryMode(true);
|
|
}
|
|
|
|
function handleManualSuccess(itemName: string) {
|
|
setSavedItemName(itemName);
|
|
}
|
|
|
|
function handleAddAnother() {
|
|
setManualEntryMode(false);
|
|
setSavedItemName(null);
|
|
}
|
|
```
|
|
|
|
### EmptyState with "Add Manually" Link
|
|
|
|
```typescript
|
|
// Source: adapted from CatalogSearchOverlay.tsx EmptyState (line 667)
|
|
function EmptyState({ hasQuery, onAddManually }: {
|
|
hasQuery: boolean;
|
|
onAddManually: () => void;
|
|
}) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-20 px-4">
|
|
{/* existing SVG icon */}
|
|
<p className="text-sm text-gray-500 text-center mb-3">
|
|
{hasQuery ? "No items found matching your search" : "Search the catalog to find gear"}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={onAddManually}
|
|
className="text-sm text-gray-500 hover:text-gray-700 underline underline-offset-2"
|
|
>
|
|
{hasQuery ? "Can't find it? Add manually" : "Add Manually"}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## State of the Art
|
|
|
|
| Old Approach | Current Approach | When Changed | Impact |
|
|
|--------------|------------------|--------------|--------|
|
|
| Static EmptyState (no action) | EmptyState with "Add Manually" CTA | Phase 23 | Removes dead end for missing catalog items |
|
|
| No manual path in catalog flow | Inline ManualEntryForm in overlay | Phase 23 | Completes CATFLOW requirements |
|
|
|
|
**What changes in Phase 23:**
|
|
- `EmptyState` — accepts `onAddManually` prop, gains a text button
|
|
- `CatalogSearchOverlay` — gains `manualEntryMode` + `savedItemName` local state, renders ManualEntryForm or SuccessCard conditionally
|
|
- New file: `ManualEntryForm.tsx`
|
|
|
|
## Open Questions
|
|
|
|
1. **Back arrow context-sensitivity**
|
|
- What we know: CatalogSearchOverlay header already has a back arrow that calls `closeCatalogSearch()`. ManualEntryForm needs to go back to results, not close the overlay (D-04).
|
|
- What's unclear: Whether the planner should (a) make the header back arrow context-aware based on `manualEntryMode`, or (b) have ManualEntryForm render its own internal back arrow in the form header and keep the outer header arrow always closing.
|
|
- Recommendation: Option (b) — ManualEntryForm has its own back arrow row (like CatalogSearchOverlay's context text row) to preserve single-responsibility. The planner should specify this explicitly.
|
|
|
|
2. **"Add Manually" link in thread mode**
|
|
- What we know: `catalogSearchMode` can be "collection" or "thread". The CONTEXT.md does not restrict manual entry to collection mode only.
|
|
- What's unclear: If the user is in "thread" mode (adding candidates), does "Add Manually" create a standalone collection item (D-08) or a thread candidate?
|
|
- Recommendation: D-08 locks this to `POST /api/items` (collection item), regardless of mode. The planner should note this explicitly to avoid an implementation where thread mode triggers candidate creation instead.
|
|
|
|
## Environment Availability
|
|
|
|
Step 2.6: SKIPPED — phase is purely frontend code changes with no external tool or service dependencies beyond the existing dev environment.
|
|
|
|
## Validation Architecture
|
|
|
|
### Test Framework
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Framework | Bun test (unit/integration) + Playwright (E2E) |
|
|
| Config file | `bunfig.toml` (Bun default), `playwright.config.ts` |
|
|
| Quick run command | `bun test tests/services/item.service.test.ts` |
|
|
| Full suite command | `bun test && bun run test:e2e` |
|
|
|
|
### Phase Requirements → Test Map
|
|
|
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
|
|--------|----------|-----------|-------------------|-------------|
|
|
| CATFLOW-07 | ManualEntryForm submits `POST /api/items` without `globalItemId` | Integration (service) | `bun test tests/services/item.service.test.ts` | Partial — item creation without globalItemId covered by existing "only name and categoryId are required" test |
|
|
| CATFLOW-07 | "Add Manually" link visible in EmptyState | E2E (smoke) | `bun run test:e2e` (collection.spec.ts) | Needs Wave 0 addition |
|
|
| CATFLOW-07 | ManualEntryForm renders in overlay on click | E2E | `bun run test:e2e` | Needs Wave 0 addition |
|
|
| CATFLOW-07 | Back arrow returns to search results | E2E | `bun run test:e2e` | Needs Wave 0 addition |
|
|
| CATFLOW-08 | "Submit to Catalog?" shows "Coming soon" toast | E2E | `bun run test:e2e` | Needs Wave 0 addition |
|
|
| CATFLOW-08 | Success card shown after manual save | E2E | `bun run test:e2e` | Needs Wave 0 addition |
|
|
|
|
> Note: The backend `createItem` service already has tests covering the standalone item path (no `globalItemId`). The existing test "only name and categoryId are required" (item.service.test.ts line 44) covers CATFLOW-07 at the service layer. The gaps are E2E tests covering the new UI flow.
|
|
|
|
### Sampling Rate
|
|
|
|
- **Per task commit:** `bun test tests/services/item.service.test.ts`
|
|
- **Per wave merge:** `bun test`
|
|
- **Phase gate:** `bun test && bun run test:e2e` green before `/gsd:verify-work`
|
|
|
|
### Wave 0 Gaps
|
|
|
|
- [ ] `e2e/collection.spec.ts` — add test cases for manual entry flow (Add Manually link, ManualEntryForm render, back navigation, success card, Submit to Catalog toast)
|
|
|
|
*(Service-level test infrastructure covers the backend path — no new test files needed there. E2E additions only.)*
|
|
|
|
## Project Constraints (from CLAUDE.md)
|
|
|
|
All directives from CLAUDE.md that apply to this phase:
|
|
|
|
| Constraint | Impact on Phase 23 |
|
|
|-----------|-------------------|
|
|
| Reuse existing components (CategoryPicker, ImageUpload, etc.) | ManualEntryForm MUST use CategoryPicker and ImageUpload — not plain `<select>` or raw file inputs |
|
|
| Prices stored as cents (`priceCents: integer`) | Convert dollars → cents with `Math.round(val * 100)` before sending to API |
|
|
| Path alias `@/*` maps to `./src/*` | Use `@/client/...` imports in new component |
|
|
| TanStack Router file-based routes — routeTree.gen.ts never edited manually | No new routes in this phase (none needed) |
|
|
| Tailwind CSS v4 | Use Tailwind for all styling — no inline styles |
|
|
| `toast()` from sonner for notifications | "Coming soon" and error messages use sonner toast |
|
|
| Bun test runner (`bun test`) | Tests written as Bun tests, not Jest/Vitest |
|
|
| Tabs, double quotes (Biome lint) | Follow formatting — run `bun run lint` before commit |
|
|
| UIStore for panel/dialog state only; server data lives in React Query | `manualEntryMode` and `savedItemName` are local state, not UIStore — correct by design |
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
|
|
- `src/client/components/CatalogSearchOverlay.tsx` — Full file read; confirmed local state pattern, EmptyState location (line 667), existing reset effect, framer-motion usage
|
|
- `src/client/components/ItemForm.tsx` — Full file read; confirmed field patterns, validation logic, CategoryPicker/ImageUpload usage
|
|
- `src/client/components/AddToCollectionModal.tsx` — Full file read; confirmed lightweight form pattern, purchasePriceCents field, toast.success pattern
|
|
- `src/client/hooks/useItems.ts` — Full file read; confirmed `useCreateItem()` signature and query invalidation
|
|
- `src/shared/schemas.ts` — Full file read; confirmed `globalItemId: z.number().int().positive().optional()` (line 13)
|
|
- `src/client/stores/uiStore.ts` — Full file read; confirmed overlay open/close state, no manualEntryMode currently
|
|
- `.planning/phases/23-manual-entry-fallback/23-CONTEXT.md` — Full read; all locked decisions documented
|
|
- `tests/services/item.service.test.ts` — Partial read; confirmed existing service test coverage for standalone item creation
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
|
|
- N/A — this phase is entirely within the existing codebase with no external library research needed
|
|
|
|
### Tertiary (LOW confidence)
|
|
|
|
- N/A
|
|
|
|
## Metadata
|
|
|
|
**Confidence breakdown:**
|
|
- Standard stack: HIGH — all libraries are already in the project, versions verified by reading source files
|
|
- Architecture: HIGH — patterns read directly from existing components; no speculation
|
|
- Pitfalls: HIGH — derived from reading actual code (reset effect, CategoryPicker value=0 issue in AddToCollectionModal)
|
|
|
|
**Research date:** 2026-04-06
|
|
**Valid until:** 2026-07-06 (stable — no fast-moving external dependencies)
|