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
12 KiB
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.tsxline numbers unchanged (146 select-part attr, 147 aria-disabled, 96 "Yukleniyor...", 119 empty-state text, 121 "Geri don" button). schema-viewer.tsxskeleton block: FN-400 noted:72-86; theif (isLoading)branch on current worktree returns through line 94 (block opens at:72and the closing brace is at:94). Useschema-viewer.tsx:72-86going 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-testidconvention). - Project memory
MEMORY.md(≈ lines 257–268 of root index):Canonical FN-367/FN-368 selectorsblock — kept in sync via the samefn_memory_appendcall 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).