17 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 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 22-add-from-catalog-thread-integration | 02 | execute | 2 |
|
|
false |
|
|
Purpose: CATFLOW-05 -- users can add catalog items as thread candidates from search. CATFLOW-06 -- resolution already works, confirmed by existing tests. Output: Working add-to-thread flow, complete Phase 22 catalog integration.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/22-add-from-catalog-thread-integration/22-CONTEXT.md @.planning/phases/22-add-from-catalog-thread-integration/22-RESEARCH.md @.planning/phases/22-add-from-catalog-thread-integration/22-01-SUMMARY.mdFrom src/client/stores/uiStore.ts (after Plan 01):
addToThreadModal: { open: boolean; globalItemId: number | null; globalItemName: string | null };
openAddToThread: (globalItemId: number, globalItemName: string) => void;
closeAddToThread: () => void;
catalogSessionThreadId: number | null;
setCatalogSessionThreadId: (id: number | null) => void;
From src/client/hooks/useThreads.ts:
export function useThreads(includeResolved = false)
// Returns ThreadListItem[] with: id, name, status, categoryId, categoryName, candidateCount
export function useCreateThread()
// mutationFn: (data: { name: string; categoryId: number }) => apiPost<ThreadListItem>("/api/threads", data)
// onSuccess invalidates ["threads"]
From src/client/hooks/useCandidates.ts:
export function useCreateCandidate(threadId: number)
// mutationFn: (data: CreateCandidate & { imageFilename?: string }) => apiPost(...)
// NOTE: threadId is a hook parameter, not in mutation payload
// For dynamic threadId (new thread flow), use apiPost directly instead
From src/shared/schemas.ts:
export const createCandidateSchema = z.object({
name: z.string().min(1, "Name is required"),
weightGrams: z.number().nonnegative().optional(),
priceCents: z.number().int().nonnegative().optional(),
categoryId: z.number().int().positive(),
notes: z.string().optional(),
productUrl: z.string().url().optional().or(z.literal("")),
imageFilename: z.string().optional(),
imageSourceUrl: z.string().url().optional().or(z.literal("")),
status: candidateStatusSchema.optional(),
pros: z.string().optional(),
cons: z.string().optional(),
globalItemId: z.number().int().positive().optional(),
});
From src/client/lib/api.ts:
export async function apiPost<T>(url: string, body: unknown): Promise<T>
From src/client/hooks/useGlobalItems.ts:
export function useGlobalItem(id: number | null)
// Returns globalItem with: id, brand, model, category, weightGrams, priceCents, imageUrl, description, ownerCount
// NOTE: Already has `enabled: id != null` guard built in -- safe to call with null globalItemId
This modal has two modes: "pick" (select existing thread) and "create" (new thread + candidate).
Component structure:
import { useState, useEffect } from "react";
import { toast } from "sonner";
import { useCategories } from "../hooks/useCategories";
import { useGlobalItem } from "../hooks/useGlobalItems";
import { useCreateThread, useThreads } from "../hooks/useThreads";
import { apiPost } from "../lib/api";
import { useUIStore } from "../stores/uiStore";
import { useQueryClient } from "@tanstack/react-query";
State management:
- Read from UIStore:
addToThreadModal(open, globalItemId, globalItemName),closeAddToThread,catalogSessionThreadId,setCatalogSessionThreadId - Local state:
mode("pick" | "create"),selectedThreadId(number | null),newThreadName(string),newThreadCategoryId(number | null),isSubmitting(boolean),error(string | null) - Fetch:
useThreads()for thread list,useCategories()for new thread category dropdown,useGlobalItem(globalItemId)for the global item data needed to create the candidate. NOTE:useGlobalItemalready hasenabled: id != nullguard built in, so passingglobalItemId(which may be null when modal is closed) is safe -- no additional enabled guard needed. useCreateThread()for thread creation,useQueryClient()for manual invalidation
Initialization logic (per D-12, D-19):
- When modal opens (
openbecomes true), ifcatalogSessionThreadIdis set, pre-select it in the thread picker (selectedThreadId = catalogSessionThreadId) - Filter threads to active only:
threads?.filter((t) => t.status === "active") ?? [] - If no active threads exist, auto-switch to "create" mode (per D-09 empty state)
"Pick" mode UI (per D-06, D-07):
- Header: "Add to Thread"
- Subheading: show
globalItemNamein<p className="text-sm text-gray-500 mb-4"> - Thread dropdown
<select>:- Each active thread as
<option value={t.id}>{t.name} ({t.categoryName})</option>-- show category alongside name per discretion - Last option:
<option value="new">+ New Thread...</option> - When "new" is selected, switch
modeto "create"
- Each active thread as
- Submit button: "Add as Candidate", disabled during
isSubmitting - Cancel button: calls
closeAddToThread()
"Create" mode UI (per D-11, D-13):
- Header: "New Thread + Candidate"
- Subheading: show
globalItemName - Thread name input: same as CreateThreadModal
- Category dropdown: same as CreateThreadModal (from
useCategories()) - Submit button: "Create & Add", disabled during
isSubmitting - Cancel button: goes back to "pick" mode if active threads exist, otherwise calls
closeAddToThread() - Back link: "Back to thread picker" if active threads exist
Submit handler for "pick" mode:
async function handleAddToExistingThread() {
if (!selectedThreadId || !globalItemId) return;
setIsSubmitting(true);
setError(null);
try {
const thread = activeThreads.find((t) => t.id === selectedThreadId);
await apiPost(`/api/threads/${selectedThreadId}/candidates`, {
name: globalItemName ?? "Unknown Item",
globalItemId,
categoryId: thread?.categoryId ?? categories?.[0]?.id ?? 1,
weightGrams: globalItem?.weightGrams ?? undefined,
priceCents: globalItem?.priceCents ?? undefined,
});
queryClient.invalidateQueries({ queryKey: ["threads"] });
queryClient.invalidateQueries({ queryKey: ["threads", selectedThreadId] });
setCatalogSessionThreadId(selectedThreadId);
toast.success(`Added to "${thread?.name ?? "thread"}"`);
closeAddToThread();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to add candidate");
} finally {
setIsSubmitting(false);
}
}
Use apiPost directly instead of useCreateCandidate hook because useCreateCandidate(threadId) requires threadId at hook initialization time. For the "pick" flow the threadId changes with selection. Using apiPost directly avoids this pitfall (per RESEARCH.md Pitfall 1).
Submit handler for "create" mode (per D-11):
async function handleCreateThreadAndAdd() {
const trimmedName = newThreadName.trim();
if (!trimmedName || !newThreadCategoryId || !globalItemId) return;
setIsSubmitting(true);
setError(null);
try {
const thread = await createThread.mutateAsync({ name: trimmedName, categoryId: newThreadCategoryId });
await apiPost(`/api/threads/${thread.id}/candidates`, {
name: globalItemName ?? "Unknown Item",
globalItemId,
categoryId: newThreadCategoryId,
weightGrams: globalItem?.weightGrams ?? undefined,
priceCents: globalItem?.priceCents ?? undefined,
});
queryClient.invalidateQueries({ queryKey: ["threads"] });
setCatalogSessionThreadId(thread.id);
toast.success(`Created "${trimmedName}" with first candidate`);
closeAddToThread();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create thread");
} finally {
setIsSubmitting(false);
}
}
Form reset: useEffect watching open -- when it becomes false, reset all local state (mode back to "pick", clear name, selectedThreadId, error).
Modal rendering: Follow CreateThreadModal pattern:
fixed inset-0 z-50 flex items-center justify-center bg-black/50- onClick backdrop = close
- Escape key = close
- Inner:
w-full max-w-md bg-white rounded-xl shadow-xl p-6,e.stopPropagation()
Add to root layout (src/client/routes/__root.tsx):
Add import:
import { AddToThreadModal } from "../components/AddToThreadModal";
Add <AddToThreadModal /> right after <AddToCollectionModal /> in the JSX.
cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -5 && bun test tests/services/thread.service.test.ts 2>&1 | tail -10
<acceptance_criteria>
- src/client/components/AddToThreadModal.tsx exists with export function AddToThreadModal
- AddToThreadModal.tsx contains useThreads import
- AddToThreadModal.tsx contains useCreateThread import
- AddToThreadModal.tsx contains useGlobalItem import
- AddToThreadModal.tsx contains apiPost import from ../lib/api
- AddToThreadModal.tsx contains setCatalogSessionThreadId
- AddToThreadModal.tsx contains catalogSessionThreadId
- AddToThreadModal.tsx contains toast.success
- AddToThreadModal.tsx contains globalItemId in the apiPost call body
- AddToThreadModal.tsx contains mode state with "pick" and "create" values
- AddToThreadModal.tsx contains + New Thread... option text
- src/client/routes/__root.tsx contains import { AddToThreadModal }
- src/client/routes/__root.tsx contains <AddToThreadModal />
- bun run build exits with code 0
- bun test tests/services/thread.service.test.ts passes -- confirms CATFLOW-06 regression coverage (resolveThread with globalItemId candidate creates reference item, test at line 704)
</acceptance_criteria>
AddToThreadModal supports picking an existing active thread and creating a new thread with first candidate. Session thread tracking persists across adds. Build passes. Thread service tests pass confirming CATFLOW-06 (resolveThread with catalog-linked candidate) is covered.
**Flow 1: Add to Collection from catalog search**
1. Click the FAB (floating action button)
2. Select "Add to Collection"
3. Catalog search overlay opens with "Adding to Collection" context
4. Search for an item and click "Add" on a card
5. Verify: AddToCollectionModal opens with category dropdown, notes field, purchase price field
6. Select a category, optionally fill notes/price, click "Add to Collection"
7. Verify: toast "Added to Collection" appears, modal closes, overlay stays open
8. Verify: item appears in collection (navigate to /collection to check)
**Flow 2: Add to Collection from global item detail page**
1. Click a catalog card to navigate to its detail page (`/global-items/:id`)
2. Verify: both "Add to Collection" and "Add to Thread" buttons are visible
3. Click "Add to Collection"
4. Verify: same modal opens, submit works, toast appears
**Flow 3: Add to Thread from catalog search (existing thread)**
1. Ensure at least one active thread exists (create one via Planning tab if needed)
2. Click FAB > "Start Thread"
3. Search for an item and click "Add"
4. Verify: thread picker modal opens listing active threads with category names
5. Select a thread and click "Add as Candidate"
6. Verify: toast "Added to [Thread Name]" appears
7. Click "Add" on another item -- verify the previously selected thread is pre-selected
**Flow 4: New Thread creation from thread picker**
1. In thread mode, click "Add" on a catalog card
2. In the thread picker dropdown, select "+ New Thread..."
3. Verify: form switches to show thread name + category fields
4. Fill in thread name, select category, click "Create & Add"
5. Verify: toast "Created [name] with first candidate" appears
6. Click "Add" on another card -- verify the new thread is pre-selected (session memory)
**Flow 5: Add to Thread from global item detail page**
1. Navigate to a global item detail page
2. Click "Add to Thread"
3. Verify: thread picker modal opens, same as Flow 3
**Flow 6: Thread resolution regression (CATFLOW-06)**
1. Go to a thread that has a catalog-linked candidate (from Flow 3/4)
2. Resolve the thread by selecting that candidate
3. Verify: a new item appears in collection with the global item data
Type "approved" or describe any issues found
1. `bun run build` passes with no type errors
2. `bun test tests/services/thread.service.test.ts` passes (CATFLOW-06 regression -- resolveThread with globalItemId)
3. Add-to-collection flow works from both entry points
4. Add-to-thread flow works with existing threads and new thread creation
5. Session thread memory works within a search session
6. Thread resolution with catalog-linked candidate creates reference item
<success_criteria>
- CATFLOW-05 is functional: user can add catalog items as thread candidates from search
- CATFLOW-06 is verified: resolving a catalog-linked candidate creates a reference item (confirmed by
bun test tests/services/thread.service.test.tsand manual Flow 6) - AddToThreadModal supports existing thread selection AND new thread + candidate creation
- Session thread tracking remembers selection within a catalog search session
- All flows accessible from both catalog search overlay and global item detail page </success_criteria>