Commits merged: - feat(FN-403): Phase-1 parts panel selector & route manifest for FN-368 Files changed: MANIFEST-FN-403.md | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) Fusion-Task-Id: FN-403
165 lines
12 KiB
Markdown
165 lines
12 KiB
Markdown
# FN-368 Parts Panel Selector Manifest
|
||
|
||
Source: FN-403 recon on worktree branch `fusion/fn-403` rebased onto `origin/dev`,
|
||
HEAD `d684e7f` (fix(pl24): pass Ford VIN BOM detail URL through fetchP4Page baseUrl prefix).
|
||
`apps/web/src/components/schema/parts-panel.tsx` content is **unchanged** from the FN-368 ship
|
||
commit `0ce68a7` (feat(FN-368): parts panel loading state, inert unavailable rows, escape hatch).
|
||
Sibling to `.fusion/tasks/FN-400/MANIFEST.md` (FN-367 / VIN-decode half).
|
||
Conventions: see project memory `frontend-testability-hook-convention` — `apps/web` does **not**
|
||
use `data-testid`; hook priority is (1) `data-faro-user-action-name`, (2) ARIA role + accessible
|
||
name, (3) visible text.
|
||
|
||
`<PartsPanel>` is always mounted via `<SchemaViewer>`. SchemaViewer mounts PartsPanel at two
|
||
sites:
|
||
- `apps/web/src/components/schema/schema-viewer.tsx:100` — no-schema fallback branch
|
||
(`if (!schemaPic)`, e.g. PL24 bomOverviewlist pages).
|
||
- `apps/web/src/components/schema/schema-viewer.tsx:168` — split-pane right column (default).
|
||
|
||
`grep -n data-testid apps/web/src/components/schema/parts-panel.tsx` → **0 matches**.
|
||
|
||
---
|
||
|
||
## Route 1 — primary post-VIN-decode flow
|
||
|
||
```
|
||
Route: /dashboard/vehicles_/$id/categories_/$categoryId
|
||
(file: apps/web/src/routes/dashboard/vehicles_/$id/categories_/$categoryId.tsx, line: 44)
|
||
Live example URL: https://sase.tr/dashboard/vehicles/<vehicleId>/categories/<categoryId>
|
||
(slug discovery: go to https://sase.tr/dashboard/search, paste a known-good
|
||
VIN, the decoded landing page links each category leaf via this route)
|
||
Container: getByRole('heading', { name: 'Parcalar' }) → closest div.flex.h-full.flex-col.overflow-hidden
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 95 for loading-state heading inside
|
||
div at line 94; line 112 for normal-state heading inside div at line 110)
|
||
Interactive elements:
|
||
- Parts row, available — selector: [data-faro-user-action-name="select-part"][role="button"],
|
||
role: button (Turkish accessible name is part.name + optional remark/modelCodes),
|
||
expected on click: useSchemaStore.setSelectedGroup(group) toggles selection,
|
||
scrolls the matched row into view via row.scrollIntoView({ behavior: "smooth", block: "center" }),
|
||
triggers hotspot highlight on the schema image,
|
||
mobile hit-target: ≈36 px (td has px-3 py-2, text-sm) — FLAG <44 px
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 146 for data-faro attr, 148 for role, 149 for tabIndex, 85-88 for scrollIntoView, 174-179 for onClick)
|
||
- Parts row, unavailable (FN-368 inert, MUST NOT be rage-clickable) — selector:
|
||
[data-faro-user-action-name="select-part"][aria-disabled="true"],
|
||
role: (no role attribute — omitted when unavailable), tabIndex={-1},
|
||
expected on click: NOTHING — onClick / onKeyDown / onMouseEnter / onMouseLeave are all undefined for unavailable rows,
|
||
mobile hit-target: ≈36 px (still rendered, opacity-40) — N/A for rage-click verification (inert)
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 146 for data-faro attr, 147 for aria-disabled, 148-149 for role/tabIndex, 158 for opacity-40 class, 171-187 for stripped handlers)
|
||
- OEM copy button — selector (scope to a parts row first): getByRole('button') matching the inline
|
||
<button type="button"> that contains a <Copy> lucide icon (no Faro hook, no aria-label),
|
||
role: button (no accessible name; icon-only),
|
||
expected on click: e.stopPropagation() → navigator.clipboard.writeText(part.oemCode) →
|
||
setCopiedId(part.id) flips the icon from <Copy> to <Check className="text-green-500"> for 1500 ms →
|
||
api.post('/analytics/oem-copy', {oemCode, partId, vehicleId, categoryId}) (errors swallowed) →
|
||
capture('oem_code_copied', {oem_code, part_id, vehicle_id, category_id}) PostHog event,
|
||
mobile hit-target: ≈18×18 px (p-0.5 + size-3.5) — FLAG <44 px
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 198 for <button>, 200-204 for onClick, 205-210 for icon swap, 56-79 for copyOemCode handler)
|
||
- Empty-state escape (FN-368) — selector: getByRole('button', { name: 'Geri don' }),
|
||
role: button (accessible name "Geri don", variant="outline"),
|
||
expected on click: window.history.back() — only renders when parts.length === 0,
|
||
mobile hit-target: ≈40 px (Button default sm h-9 ≈ 36 px; with outline border + py-2 effective ≈40 px) — borderline, document
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 120 for <Button onClick>, 121 for label, 117 for parts.length===0 branch guard)
|
||
Loading state: getByText('Yukleniyor...') (no diacritics) + 8 sibling <Skeleton className="h-10 w-full">
|
||
keyed s0..s7. Branch entered when isLoading prop is true. Note: <SchemaViewer>'s page-level
|
||
skeleton at schema-viewer.tsx:72-86 usually fires before PartsPanel's own loading branch.
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 96 for "Yukleniyor...", 16 for SKELETON_ROW_KEYS, 99-103 for Skeleton map, 89 for `if (isLoading)` guard)
|
||
Empty state: getByText('Bu kategori icin parca bulunamadi.') (no diacritics) + "Geri don" button.
|
||
Branch entered when parts.length === 0 after isLoading clears.
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 119 for text, 121 for button label, 117 for parts.length===0 guard)
|
||
Feedback affordance (FN-368 fix): NONE — failures bubble to SchemaViewer skeleton at schema-viewer.tsx:72-86.
|
||
FN-368 (commit 0ce68a7) shipped (a) aria-disabled="true" + stripped handlers on unavailable rows,
|
||
(b) "Yukleniyor..." loading branch + 8 skeleton rows, (c) "Geri don" empty-state escape. No toast,
|
||
no inline error UI. `use-parts` fetch failures bubble up to <SchemaViewer>'s page-level
|
||
`if (isLoading)` skeleton block. `grep -n toast apps/web/src/components/schema/*.tsx
|
||
apps/web/src/hooks/use-parts.ts` → 0 matches; PartsPanel has no toast call site.
|
||
(file: apps/web/src/components/schema/schema-viewer.tsx, line: 72 for `if (isLoading)`, 73-85 for skeleton return, 86 for closing brace)
|
||
data-testid introduced by FN-368: NONE (sase/web convention — see project memory
|
||
`frontend-testability-hook-convention`).
|
||
(verified: `grep -n data-testid apps/web/src/components/schema/parts-panel.tsx` → 0 matches at HEAD d684e7f)
|
||
```
|
||
|
||
---
|
||
|
||
## Route 2 — catalog drill-down (brand → model → categories)
|
||
|
||
```
|
||
Route: /dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId
|
||
(file: apps/web/src/routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId.tsx, line: 36)
|
||
Live example URL: https://sase.tr/dashboard/catalog/<brand>/<modelId>/categories/<categoryId>
|
||
[optional search params: ?body=&engine=&gearbox=&mgp=]
|
||
(slug discovery: start at https://sase.tr/dashboard/catalog, drill brand →
|
||
model → category)
|
||
Container: same — getByRole('heading', { name: 'Parcalar' })
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 95 / 112)
|
||
Interactive elements: identical to Route 1 (same <SchemaViewer><PartsPanel> children).
|
||
Mount site in this route: SchemaViewer rendered at line 225 inside `<Suspense fallback={<SchemaViewerFallback />}>`.
|
||
(file: apps/web/src/routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId.tsx, line: 225)
|
||
Loading state: same — see Route 1 block; this route additionally renders a route-level
|
||
<SchemaViewerFallback /> while the lazy chunk loads (line 224 Suspense + line 121 also).
|
||
(file: apps/web/src/routes/dashboard/catalog_/$brandName_/$modelId/categories_/$categoryId.tsx, line: 224 for Suspense, 121 for in-page fallback usage)
|
||
Empty state: same as Route 1.
|
||
Feedback affordance (FN-368 fix): NONE — failures bubble to SchemaViewer skeleton at schema-viewer.tsx:72-86.
|
||
data-testid introduced by FN-368: NONE (sase/web convention — see project memory
|
||
`frontend-testability-hook-convention`).
|
||
```
|
||
|
||
---
|
||
|
||
## Route 3 — emex catalog group leaf
|
||
|
||
```
|
||
Route: /dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId
|
||
(file: apps/web/src/routes/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId.tsx, line: 17)
|
||
Live example URL: https://sase.tr/dashboard/catalog/emex/<catalogCode>/<vehicleId>/groups/<groupId>
|
||
(slug discovery: start at https://sase.tr/dashboard/catalog, pick an
|
||
emex-backed brand → model → vehicle → group)
|
||
Container: same — getByRole('heading', { name: 'Parcalar' })
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 95 / 112)
|
||
Interactive elements: identical to Route 1.
|
||
Mount site in this route: SchemaViewer rendered at line 83.
|
||
(file: apps/web/src/routes/dashboard/catalog_/emex/$catalogCode_/$vehicleId_/groups/$groupId.tsx, line: 83)
|
||
Loading state: same as Route 1.
|
||
Empty state: same as Route 1.
|
||
Feedback affordance (FN-368 fix): NONE — failures bubble to SchemaViewer skeleton at schema-viewer.tsx:72-86.
|
||
data-testid introduced by FN-368: NONE (sase/web convention — see project memory
|
||
`frontend-testability-hook-convention`).
|
||
```
|
||
|
||
---
|
||
|
||
## Route 4 — pcat catalog group leaf
|
||
|
||
```
|
||
Route: /dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId
|
||
(file: apps/web/src/routes/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId.tsx, line: 17)
|
||
Live example URL: https://sase.tr/dashboard/catalog/pcat/<catalogId>/<modelId>/<carId>/groups/<groupId>
|
||
(slug discovery: start at https://sase.tr/dashboard/catalog, pick a
|
||
pcat-backed brand → model → car → group)
|
||
Container: same — getByRole('heading', { name: 'Parcalar' })
|
||
(file: apps/web/src/components/schema/parts-panel.tsx, line: 95 / 112)
|
||
Interactive elements: identical to Route 1.
|
||
Mount site in this route: SchemaViewer rendered at line 100.
|
||
(file: apps/web/src/routes/dashboard/catalog_/pcat/$catalogId_/$modelId_/$carId_/groups/$groupId.tsx, line: 100)
|
||
Loading state: same as Route 1.
|
||
Empty state: same as Route 1.
|
||
Feedback affordance (FN-368 fix): NONE — failures bubble to SchemaViewer skeleton at schema-viewer.tsx:72-86.
|
||
data-testid introduced by FN-368: NONE (sase/web convention — see project memory
|
||
`frontend-testability-hook-convention`).
|
||
```
|
||
|
||
---
|
||
|
||
## Drift vs FN-400 baseline
|
||
|
||
FN-400 captured selectors against `main @ 08dbde7`. Re-verifying on current worktree HEAD `d684e7f`:
|
||
- All `parts-panel.tsx` line numbers unchanged (146 select-part attr, 147 aria-disabled, 96 "Yukleniyor...", 119 empty-state text, 121 "Geri don" button).
|
||
- `schema-viewer.tsx` skeleton block: FN-400 noted `:72-86`; the `if (isLoading)` branch on current worktree returns through line 94 (block opens at `:72` and the closing brace is at `:94`). Use **`schema-viewer.tsx:72-86`** going forward.
|
||
- All four route files (mount sites in step 3 of PROMPT.md) confirmed present and mounting `<SchemaViewer>`.
|
||
|
||
## Cross-references
|
||
|
||
- Project memory: `frontend-testability-hook-convention` (sase/web no-`data-testid` convention).
|
||
- Project memory `MEMORY.md` (≈ lines 257–268 of root index): `Canonical FN-367/FN-368 selectors` block — kept in sync via the same `fn_memory_append` call that wrote the compact version of this manifest.
|
||
- Sibling: `.fusion/tasks/FN-400/MANIFEST.md` (FN-367 / VIN-decode half).
|
||
- Consumer: FN-393 (Playwright verification — to be re-specified against this manifest).
|
||
- Source commit for the fix: `0ce68a7` — `feat(FN-368): parts panel loading state, inert unavailable rows, escape hatch (gitea #10)`.
|