280 lines
11 KiB
Markdown
280 lines
11 KiB
Markdown
---
|
|
phase: 20-fab-full-screen-catalog-search
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- src/server/services/tag.service.ts
|
|
- src/server/routes/tags.ts
|
|
- src/server/index.ts
|
|
- src/client/stores/uiStore.ts
|
|
- src/client/hooks/useTags.ts
|
|
- src/client/hooks/useGlobalItems.ts
|
|
- tests/services/tag.service.test.ts
|
|
- tests/routes/tags.test.ts
|
|
autonomous: true
|
|
requirements:
|
|
- CATFLOW-01
|
|
- CATFLOW-02
|
|
|
|
must_haves:
|
|
truths:
|
|
- "GET /api/tags returns all tags from the database"
|
|
- "GET /api/global-items endpoint is reachable (route registered)"
|
|
- "UIStore exposes fabMenu and catalogSearch state slices"
|
|
- "useGlobalItems hook supports optional tags parameter"
|
|
- "useTags hook fetches and caches tag data"
|
|
artifacts:
|
|
- path: "src/server/services/tag.service.ts"
|
|
provides: "getAllTags function"
|
|
exports: ["getAllTags"]
|
|
- path: "src/server/routes/tags.ts"
|
|
provides: "GET /api/tags endpoint"
|
|
exports: ["tagRoutes"]
|
|
- path: "src/client/stores/uiStore.ts"
|
|
provides: "FAB menu + catalog search state"
|
|
contains: "fabMenuOpen"
|
|
- path: "src/client/hooks/useTags.ts"
|
|
provides: "Tag fetching hook"
|
|
exports: ["useTags"]
|
|
- path: "src/client/hooks/useGlobalItems.ts"
|
|
provides: "Updated hook with tag support"
|
|
contains: "tags"
|
|
key_links:
|
|
- from: "src/server/routes/tags.ts"
|
|
to: "src/server/services/tag.service.ts"
|
|
via: "getAllTags import"
|
|
pattern: "getAllTags"
|
|
- from: "src/server/index.ts"
|
|
to: "src/server/routes/tags.ts"
|
|
via: "app.route registration"
|
|
pattern: "app\\.route.*tags"
|
|
- from: "src/server/index.ts"
|
|
to: "src/server/routes/global-items.ts"
|
|
via: "app.route registration"
|
|
pattern: "app\\.route.*global-items"
|
|
---
|
|
|
|
<objective>
|
|
Create the server-side tags endpoint, register the global-items route, extend UIStore with FAB/catalog-search state, and update client hooks for tag-aware searching.
|
|
|
|
Purpose: Provides the data layer (tags API), state management (UIStore), and hooks that Plan 02's UI components will consume.
|
|
Output: Working GET /api/tags endpoint, registered GET /api/global-items, extended UIStore, useTags hook, updated useGlobalItems hook with tag support.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
|
|
@.planning/phases/20-fab-full-screen-catalog-search/20-RESEARCH.md
|
|
|
|
@src/server/index.ts
|
|
@src/server/routes/global-items.ts
|
|
@src/server/services/global-item.service.ts
|
|
@src/db/schema.ts
|
|
@src/client/stores/uiStore.ts
|
|
@src/client/hooks/useGlobalItems.ts
|
|
@src/client/lib/api.ts
|
|
@tests/routes/global-items.test.ts
|
|
@tests/helpers/db.ts
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs -->
|
|
|
|
From src/db/schema.ts:
|
|
```typescript
|
|
export const tags = pgTable("tags", {
|
|
id: serial("id").primaryKey(),
|
|
name: text("name").notNull().unique(),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
});
|
|
```
|
|
|
|
From src/client/lib/api.ts:
|
|
```typescript
|
|
export async function apiGet<T>(path: string): Promise<T>;
|
|
```
|
|
|
|
From src/client/stores/uiStore.ts (existing pattern):
|
|
```typescript
|
|
export const useUIStore = create<UIState>((set) => ({ ... }));
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Tag service, route, route registrations, and tests</name>
|
|
<files>
|
|
src/server/services/tag.service.ts,
|
|
src/server/routes/tags.ts,
|
|
src/server/index.ts,
|
|
tests/services/tag.service.test.ts,
|
|
tests/routes/tags.test.ts
|
|
</files>
|
|
<read_first>
|
|
src/db/schema.ts,
|
|
src/server/index.ts,
|
|
src/server/routes/global-items.ts,
|
|
src/server/services/global-item.service.ts,
|
|
tests/routes/global-items.test.ts,
|
|
tests/helpers/db.ts
|
|
</read_first>
|
|
<behavior>
|
|
- Test: getAllTags returns all tags from the database as { id, name }[] (no createdAt)
|
|
- Test: getAllTags returns empty array when no tags exist
|
|
- Test: GET /api/tags returns 200 with array of tag objects
|
|
- Test: GET /api/global-items still works after route registration (existing tests pass)
|
|
</behavior>
|
|
<action>
|
|
1. Create `src/server/services/tag.service.ts`:
|
|
- Export `getAllTags(db)` function that selects `{ id: tags.id, name: tags.name }` from the `tags` table, ordered alphabetically by name (per D-17, alphabetical is a reasonable default).
|
|
- Import `tags` from `../../db/schema.ts`.
|
|
- Use the same `type Db = typeof prodDb` pattern as other services.
|
|
|
|
2. Create `src/server/routes/tags.ts`:
|
|
- Single GET "/" handler that calls `getAllTags(db)` and returns `c.json(allTags)`.
|
|
- Export as `tagRoutes`.
|
|
- Follow the exact pattern from `src/server/routes/global-items.ts` (Hono + Env type).
|
|
|
|
3. Update `src/server/index.ts`:
|
|
- Add import: `import { globalItemRoutes } from "./routes/global-items.ts";`
|
|
- Add import: `import { tagRoutes } from "./routes/tags.ts";`
|
|
- Register both routes AFTER existing route registrations (before MCP block):
|
|
`app.route("/api/global-items", globalItemRoutes);`
|
|
`app.route("/api/tags", tagRoutes);`
|
|
- CRITICAL: The global-items route file EXISTS but is NOT registered (research pitfall #1). Must add it here.
|
|
- Add public route skip in auth middleware for tags: `if (c.req.path.startsWith("/api/tags") && c.req.method === "GET") return next();`
|
|
- Add public route skip for global-items: `if (c.req.path.startsWith("/api/global-items") && c.req.method === "GET") return next();`
|
|
|
|
4. Create `tests/services/tag.service.test.ts`:
|
|
- Use `createTestDb()` from `tests/helpers/db.ts`.
|
|
- Test: returns empty array when no tags.
|
|
- Test: returns all tags as `{ id, name }` after inserting test data.
|
|
- Seed tags using `db.insert(tags).values(...)`.
|
|
|
|
5. Create `tests/routes/tags.test.ts`:
|
|
- Follow pattern from `tests/routes/global-items.test.ts`.
|
|
- Use the app with test db injection.
|
|
- Test: GET /api/tags returns 200 with JSON array.
|
|
- Test: response contains expected tags after seeding.
|
|
</action>
|
|
<verify>
|
|
<automated>bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts tests/routes/global-items.test.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- File `src/server/services/tag.service.ts` exists and exports `getAllTags`
|
|
- File `src/server/routes/tags.ts` exists and exports `tagRoutes`
|
|
- `src/server/index.ts` contains `app.route("/api/tags", tagRoutes)`
|
|
- `src/server/index.ts` contains `app.route("/api/global-items", globalItemRoutes)`
|
|
- `src/server/index.ts` contains auth skip for `/api/tags` and `/api/global-items` GET requests
|
|
- All tag tests pass: `bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts`
|
|
- Existing global-items tests still pass: `bun test tests/routes/global-items.test.ts`
|
|
</acceptance_criteria>
|
|
<done>GET /api/tags returns all tags. GET /api/global-items is registered and reachable. All tests green.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: UIStore extension + useTags hook + useGlobalItems tag support</name>
|
|
<files>
|
|
src/client/stores/uiStore.ts,
|
|
src/client/hooks/useTags.ts,
|
|
src/client/hooks/useGlobalItems.ts
|
|
</files>
|
|
<read_first>
|
|
src/client/stores/uiStore.ts,
|
|
src/client/hooks/useGlobalItems.ts,
|
|
src/client/lib/api.ts,
|
|
.planning/phases/20-fab-full-screen-catalog-search/20-CONTEXT.md
|
|
</read_first>
|
|
<action>
|
|
1. Extend `src/client/stores/uiStore.ts` per D-21, D-22, D-23:
|
|
- Add to UIState interface:
|
|
```
|
|
fabMenuOpen: boolean;
|
|
openFabMenu: () => void;
|
|
closeFabMenu: () => void;
|
|
catalogSearchOpen: boolean;
|
|
catalogSearchMode: "collection" | "thread" | null;
|
|
openCatalogSearch: (mode: "collection" | "thread") => void;
|
|
closeCatalogSearch: () => void;
|
|
```
|
|
- Add to store implementation:
|
|
```
|
|
fabMenuOpen: false,
|
|
openFabMenu: () => set({ fabMenuOpen: true }),
|
|
closeFabMenu: () => set({ fabMenuOpen: false }),
|
|
catalogSearchOpen: false,
|
|
catalogSearchMode: null,
|
|
openCatalogSearch: (mode) => set({ catalogSearchOpen: true, catalogSearchMode: mode, fabMenuOpen: false }),
|
|
closeCatalogSearch: () => set({ catalogSearchOpen: false, catalogSearchMode: null }),
|
|
```
|
|
- Note: `openCatalogSearch` also closes the FAB menu (natural flow: tap FAB -> tap option -> menu closes, overlay opens).
|
|
|
|
2. Create `src/client/hooks/useTags.ts`:
|
|
- Export `useTags()` hook using `useQuery` from `@tanstack/react-query`.
|
|
- Query key: `["tags"]`.
|
|
- Query fn: `apiGet<Tag[]>("/api/tags")` where `Tag = { id: number; name: string }`.
|
|
- Set `staleTime: 5 * 60 * 1000` (tags change rarely, cache 5 min).
|
|
- Export the `Tag` interface as well.
|
|
|
|
3. Update `src/client/hooks/useGlobalItems.ts`:
|
|
- Modify `useGlobalItems` signature to accept optional `tags?: string[]` parameter.
|
|
- Build query string using `URLSearchParams`:
|
|
```
|
|
const params = new URLSearchParams();
|
|
if (query) params.set("q", query);
|
|
if (tags && tags.length > 0) params.set("tags", tags.join(","));
|
|
const qs = params.toString();
|
|
```
|
|
- Update query key to include tags: `["global-items", query ?? "", tags ?? []]`.
|
|
- Update query fn URL: `` `/api/global-items${qs ? `?${qs}` : ""}` ``.
|
|
- Keep all other exports (`useGlobalItem`, `useLinkItem`, `useUnlinkItem`) unchanged.
|
|
</action>
|
|
<verify>
|
|
<automated>bun run lint && bun run build</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `src/client/stores/uiStore.ts` contains `fabMenuOpen` state field
|
|
- `src/client/stores/uiStore.ts` contains `catalogSearchOpen` state field
|
|
- `src/client/stores/uiStore.ts` contains `catalogSearchMode` state field
|
|
- `src/client/stores/uiStore.ts` contains `openCatalogSearch` action
|
|
- `src/client/stores/uiStore.ts` contains `closeCatalogSearch` action
|
|
- `src/client/hooks/useTags.ts` exists and exports `useTags`
|
|
- `src/client/hooks/useGlobalItems.ts` accepts `tags?: string[]` parameter
|
|
- `bun run lint` passes with no errors
|
|
- `bun run build` succeeds
|
|
</acceptance_criteria>
|
|
<done>UIStore has FAB menu + catalog search state. useTags hook fetches tags. useGlobalItems supports tag filtering. Lint and build pass.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `bun test tests/services/tag.service.test.ts tests/routes/tags.test.ts tests/routes/global-items.test.ts` -- all green
|
|
- `bun run lint` -- no errors
|
|
- `bun run build` -- succeeds
|
|
- `bun test` -- full suite green (no regressions)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
1. GET /api/tags returns all tags as JSON array
|
|
2. GET /api/global-items is registered and reachable (was missing from index.ts)
|
|
3. UIStore has fabMenuOpen, catalogSearchOpen, catalogSearchMode state
|
|
4. useTags hook works with staleTime caching
|
|
5. useGlobalItems accepts optional tags parameter for filtering
|
|
6. All existing tests pass (no regressions)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/20-fab-full-screen-catalog-search/20-01-SUMMARY.md`
|
|
</output>
|