Implement the 030-bulk-import-sources feature that adds a one-click bulk import button to load all bestiary sources at once, with real-time progress feedback in the side panel and a toast notification when the panel is closed, plus completion/failure reporting with auto-dismiss on success and persistent display on partial failure, while also hardening the bestiary normalizer to handle variable stat blocks (spell summons with special AC/HP/CR) and skip malformed monster entries gracefully
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,9 +17,12 @@
|
||||
|
||||
**Purpose**: Adapter helpers and core hook that all user stories depend on
|
||||
|
||||
- [ ] T001 Add `getAllSourceCodes()` helper to `apps/web/src/adapters/bestiary-index-adapter.ts` that returns all source codes from the bestiary index's `sources` object as a `string[]`
|
||||
- [ ] T002 Add `getBulkFetchUrl(baseUrl: string, sourceCode: string)` helper to `apps/web/src/adapters/bestiary-index-adapter.ts` that constructs `{baseUrl}bestiary-{sourceCode.toLowerCase()}.json` (ensure trailing slash normalization on baseUrl)
|
||||
- [ ] T003 Create `apps/web/src/hooks/use-bulk-import.ts` — the `useBulkImport` hook managing `BulkImportState` (status, total, completed, failed) with a `startImport(baseUrl: string, fetchAndCacheSource, isSourceCached, refreshCache)` method that: filters out already-cached sources via `isSourceCached`, fires all remaining fetches concurrently via `Promise.allSettled()`, increments completed/failed counters as each settles, calls `refreshCache()` once when all settle, and transitions status to `complete` or `partial-failure`
|
||||
- [x] T001 Add `getAllSourceCodes()` helper to `apps/web/src/adapters/bestiary-index-adapter.ts` that returns all source codes from the bestiary index's `sources` object as a `string[]`
|
||||
- [x] T002 Refactor `getDefaultFetchUrl` in `apps/web/src/adapters/bestiary-index-adapter.ts` to accept an optional `baseUrl` parameter: `getDefaultFetchUrl(sourceCode: string, baseUrl?: string): string`. When `baseUrl` is provided, construct `{baseUrl}bestiary-{sourceCode.toLowerCase()}.json` (with trailing-slash normalization). When omitted, use the existing hardcoded default. Update existing call sites (no behavior change for current callers).
|
||||
- [x] T003 Create `apps/web/src/hooks/use-bulk-import.ts` — the `useBulkImport` hook managing `BulkImportState` with a `startImport(baseUrl: string, fetchAndCacheSource, isSourceCached, refreshCache)` method. State shape: `{ status: 'idle' | 'loading' | 'complete' | 'partial-failure', total: number, completed: number, failed: number }` where `total` = ALL sources in the index (including pre-cached), `completed` = successfully loaded (fetched + pre-cached/skipped), `failed` = fetch failures. On start: set `total` to `getAllSourceCodes().length`, immediately count already-cached sources into `completed`, fire remaining fetches concurrently via `Promise.allSettled()`, increment completed/failed as each settles, call `refreshCache()` once when all settle, transition status to `complete` (failed === 0) or `partial-failure`.
|
||||
|
||||
- [x] T003a [P] Write tests in `apps/web/src/__tests__/bestiary-index-helpers.test.ts` for: `getAllSourceCodes()` returns all keys from the index's `sources` object; `getDefaultFetchUrl(sourceCode, baseUrl)` constructs correct URL with trailing-slash normalization and lowercases the source code in the filename.
|
||||
- [x] T003b [P] Write tests in `apps/web/src/__tests__/use-bulk-import.test.ts` for: starts with `idle` status; skips already-cached sources (counts them into `completed`); increments `completed` on successful fetch; increments `failed` on rejected fetch; transitions to `complete` when all succeed; transitions to `partial-failure` when any fetch fails; all-cached edge case immediately transitions to `complete` with 0 fetches; calls `refreshCache()` exactly once when all settle.
|
||||
|
||||
**Checkpoint**: Foundation ready — user story implementation can begin
|
||||
|
||||
@@ -33,10 +36,10 @@
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T004 [US1] Create `apps/web/src/components/bulk-import-prompt.tsx` — component showing descriptive text ("Load stat block data for all 102 sources at once. This will download approximately 12.5 MB..."), an editable Input pre-filled with `https://raw.githubusercontent.com/5etools-mirror-3/5etools-src/main/data/bestiary/`, and a "Load All" Button (disabled when URL is empty or import is active). On click, calls a provided `onStartImport(baseUrl)` callback.
|
||||
- [ ] T005 [US1] Add Import button (Lucide `Import` icon) to `apps/web/src/components/action-bar.tsx` in the top bar area. Clicking it calls a new `onBulkImport` callback prop.
|
||||
- [ ] T006 [US1] Add bulk import mode to `apps/web/src/components/stat-block-panel.tsx` — when a new `bulkImportMode` prop is true, render `BulkImportPrompt` instead of the normal stat block or source fetch prompt content. Pass through `onStartImport` and bulk import state props.
|
||||
- [ ] T007 [US1] Wire bulk import in `apps/web/src/App.tsx` — add `bulkImportMode` state, pass `onBulkImport` to ActionBar (sets mode + opens panel), pass `bulkImportMode` and `useBulkImport` state to StatBlockPanel, call `useBulkImport.startImport()` with `fetchAndCacheSource`, `isSourceCached`, and `refreshCache` from `useBestiary`.
|
||||
- [x] T004 [US1] Create `apps/web/src/components/bulk-import-prompt.tsx` — component showing descriptive text ("Load stat block data for all {totalSources} sources at once. This will download approximately 12.5 MB..." where `totalSources` is derived from `getAllSourceCodes().length`), an editable Input pre-filled with `https://raw.githubusercontent.com/5etools-mirror-3/5etools-src/main/data/bestiary/`, and a "Load All" Button (disabled when URL is empty/whitespace-only or import is active). On click, calls a provided `onStartImport(baseUrl)` callback.
|
||||
- [x] T005 [US1] Add Import button (Lucide `Import` icon) to `apps/web/src/components/action-bar.tsx` in the top bar area. Clicking it calls a new `onBulkImport` callback prop.
|
||||
- [x] T006 [US1] Add bulk import mode to `apps/web/src/components/stat-block-panel.tsx` — when a new `bulkImportMode` prop is true, render `BulkImportPrompt` instead of the normal stat block or source fetch prompt content. Pass through `onStartImport` and bulk import state props.
|
||||
- [x] T007 [US1] Wire bulk import in `apps/web/src/App.tsx` — add `bulkImportMode` state, pass `onBulkImport` to ActionBar (sets mode + opens panel), pass `bulkImportMode` and `useBulkImport` state to StatBlockPanel, call `useBulkImport.startImport()` with `fetchAndCacheSource`, `isSourceCached`, and `refreshCache` from `useBestiary`.
|
||||
|
||||
**Checkpoint**: User Story 1 fully functional — Import button opens panel, "Load All" fetches all sources, cached sources skipped
|
||||
|
||||
@@ -50,7 +53,7 @@
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [ ] T008 [US2] Add progress display to `apps/web/src/components/bulk-import-prompt.tsx` — when import status is `loading`, replace the "Load All" button area with a text counter ("Loading sources... {completed}/{total}") and a Tailwind-styled progress bar (`<div>` with percentage width based on `(completed + failed) / total`). The component receives `BulkImportState` as a prop.
|
||||
- [x] T008 [US2] Add progress display to `apps/web/src/components/bulk-import-prompt.tsx` — when import status is `loading`, replace the "Load All" button area with a text counter ("Loading sources... {completed}/{total}") and a Tailwind-styled progress bar (`<div>` with percentage width based on `(completed + failed) / total`). The component receives `BulkImportState` as a prop.
|
||||
|
||||
**Checkpoint**: User Stories 1 AND 2 work together — full import flow with live progress
|
||||
|
||||
@@ -64,8 +67,8 @@
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [ ] T009 [P] [US3] Create `apps/web/src/components/toast.tsx` — lightweight toast component using `ReactDOM.createPortal` to `document.body`. Renders at bottom-center with fixed positioning. Shows: message text, optional progress bar, optional dismiss button (X). Accepts `onDismiss` callback. Styled with Tailwind (dark background, rounded, shadow, z-50).
|
||||
- [ ] T010 [US3] Wire toast visibility in `apps/web/src/App.tsx` — show the toast when bulk import status is `loading` AND the stat block panel is closed (or `bulkImportMode` is false). Derive toast message from `BulkImportState`: "Loading sources... {completed}/{total}" with progress value `(completed + failed) / total`. Hide toast when panel reopens in bulk import mode.
|
||||
- [x] T009 [P] [US3] Create `apps/web/src/components/toast.tsx` — lightweight toast component using `ReactDOM.createPortal` to `document.body`. Renders at bottom-center with fixed positioning. Shows: message text, optional progress bar, optional dismiss button (X). Accepts `onDismiss` callback. Styled with Tailwind (dark background, rounded, shadow, z-50).
|
||||
- [x] T010 [US3] Wire toast visibility in `apps/web/src/App.tsx` — show the toast when bulk import status is `loading` AND the stat block panel is closed (or `bulkImportMode` is false). Derive toast message from `BulkImportState`: "Loading sources... {completed}/{total}" with progress value `(completed + failed) / total`. Hide toast when panel reopens in bulk import mode.
|
||||
|
||||
**Checkpoint**: User Story 3 functional — closing panel during import shows toast with progress
|
||||
|
||||
@@ -79,9 +82,9 @@
|
||||
|
||||
### Implementation for User Story 4
|
||||
|
||||
- [ ] T011 [US4] Add completion states to `apps/web/src/components/bulk-import-prompt.tsx` — when status is `complete`, show "All sources loaded" success message. When status is `partial-failure`, show "Loaded {completed}/{total + completed} sources ({failed} failed)" message. Include a "Done" button to reset bulk import mode.
|
||||
- [ ] T012 [US4] Add completion behavior to toast in `apps/web/src/App.tsx` — when status is `complete`, show "All sources loaded" toast with `autoDismissMs` (e.g., 3000ms) and auto-hide via `setTimeout`. When status is `partial-failure`, show count message with dismiss button, no auto-dismiss. On dismiss, reset bulk import state to `idle`.
|
||||
- [ ] T013 [US4] Add auto-dismiss support to `apps/web/src/components/toast.tsx` — accept optional `autoDismissMs` prop. When set, start a `setTimeout` on mount that calls `onDismiss` after the delay. Clear timeout on unmount.
|
||||
- [x] T011 [US4] Add completion states to `apps/web/src/components/bulk-import-prompt.tsx` — when status is `complete`, show "All sources loaded" success message. When status is `partial-failure`, show "Loaded {completed}/{total} sources ({failed} failed)" message. Include a "Done" button to reset bulk import mode.
|
||||
- [x] T012 [US4] Add completion behavior to toast in `apps/web/src/App.tsx` — when status is `complete`, show "All sources loaded" toast with `autoDismissMs` (e.g., 3000ms) and auto-hide via `setTimeout`. When status is `partial-failure`, show count message with dismiss button, no auto-dismiss. On dismiss, reset bulk import state to `idle`.
|
||||
- [x] T013 [US4] Add auto-dismiss support to `apps/web/src/components/toast.tsx` — accept optional `autoDismissMs` prop. When set, start a `setTimeout` on mount that calls `onDismiss` after the delay. Clear timeout on unmount.
|
||||
|
||||
**Checkpoint**: All user stories complete — full flow with progress, toast, and completion reporting
|
||||
|
||||
@@ -91,9 +94,9 @@
|
||||
|
||||
**Purpose**: Edge cases and cleanup
|
||||
|
||||
- [ ] T014 Disable Import button in `apps/web/src/components/action-bar.tsx` while bulk import status is `loading` to prevent double-trigger
|
||||
- [ ] T015 Handle all-cached edge case in `apps/web/src/hooks/use-bulk-import.ts` — if all sources are already cached (0 to fetch), immediately transition to `complete` status without firing any fetches
|
||||
- [ ] T016 Run `pnpm check` (knip + format + lint + typecheck + test) and fix any issues
|
||||
- [x] T014 Disable Import button in `apps/web/src/components/action-bar.tsx` while bulk import status is `loading` to prevent double-trigger
|
||||
- [x] T015 Handle all-cached edge case in `apps/web/src/hooks/use-bulk-import.ts` — if all sources are already cached (0 to fetch), immediately transition to `complete` status without firing any fetches
|
||||
- [x] T016 Run `pnpm check` (knip + format + lint + typecheck + test) and fix any issues
|
||||
|
||||
---
|
||||
|
||||
@@ -118,6 +121,7 @@
|
||||
### Parallel Opportunities
|
||||
|
||||
- T001 and T002 can run in parallel (different functions, same file)
|
||||
- T003a and T003b can run in parallel with each other and with T003 (different files)
|
||||
- T009 (toast component) can run in parallel with T004–T008 (different files)
|
||||
- T011 and T013 can run in parallel (different files)
|
||||
|
||||
@@ -149,7 +153,7 @@ Task: T009 "Create toast.tsx" (US3) — runs in parallel, different file
|
||||
2. Complete Phase 2: US1 — Import button, prompt, fetch logic
|
||||
3. Complete Phase 3: US2 — Progress counter + bar in panel
|
||||
4. **STOP and VALIDATE**: Full import flow works with progress feedback
|
||||
5. This delivers the core user value with 10 tasks (T001–T008)
|
||||
5. This delivers the core user value with tasks T001–T008 (plus T003a, T003b tests)
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
@@ -168,4 +172,4 @@ Task: T009 "Create toast.tsx" (US3) — runs in parallel, different file
|
||||
- US1 and US2 share `bulk-import-prompt.tsx` — US2 extends the component from US1
|
||||
- US3's toast component (T009) is fully independent and can be built any time
|
||||
- US4 adds completion behavior to both panel (from US1) and toast (from US3)
|
||||
- No test tasks included — not explicitly requested in spec
|
||||
- Test tasks (T003a, T003b) cover foundational helpers and hook logic
|
||||
|
||||
Reference in New Issue
Block a user