Files
sase.tr/MANIFEST-FN-403.md
Fusion 713ef987a0 feat(FN-403): Phase-1 parts panel selector & route manifest for FN-368
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
2026-05-16 11:24:00 +03:00

12 KiB
Raw Blame History

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-conventionapps/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.tsx0 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 257268 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: 0ce68a7feat(FN-368): parts panel loading state, inert unavailable rows, escape hatch (gitea #10).