Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

ViewSwitcher renders NO icon for chart and gantt views: both icon names were dropped from lucide's icons record #5586

Closed

Description

@os-sales

Measured while implementing #5321 on origin/main @ f2158ec86. Filed unassigned, not claimed, and deliberately not fixed there — #5321 owns the missing tree KEY in the same map, and this is a different defect (a key that is present but whose VALUE no longer resolves), needing new icon names chosen rather than a key added.

What was measured

ViewSwitcher.resolveIcon (packages/plugin-view/src/ViewSwitcher.tsx) turns an icon NAME into a component by PascalCasing it and looking it up in lucide's icons record:

const iconName = toPascalCase(name);
const mapped = iconNameMap[iconName] || iconName;
return (icons as any)[mapped] || null;

Two of the names ObjectView's iconMap supplies do not exist in that record on the installed lucide-react@1.31.0. Probed directly against the installed package:

kanban    kanban         -> Kanban           RESOLVES
calendar  calendar       -> Calendar         RESOLVES
map       map            -> Map              RESOLVES
gallery   layout-grid    -> LayoutGrid       RESOLVES
timeline  activity       -> Activity         RESOLVES
gantt     gantt-chart    -> GanttChart       *** MISSING ***
grid      table          -> Table            RESOLVES
list      list           -> List             RESOLVES
detail    file-text      -> FileText         RESOLVES
chart     bar-chart-3    -> BarChart3        *** MISSING ***
tree      list-tree      -> ListTree         RESOLVES

The cause is that lucide keeps the old spellings as deprecated NAMED EXPORTS while dropping them from the icons record that a by-name lookup reads. Measured on the same version:

name named export in icons
BarChart3 yes no
GanttChart yes no
GanttChartSquare yes no
ChartColumn yes yes
ChartGantt yes yes

So the compiler is no help here: ViewSwitcher.tsx imports BarChart3 and GanttChartSquare by name for its own DEFAULT_VIEW_ICONS and type-checks clean, while the by-name path beside it silently misses.

User-visible effect

getViewIcon does not fall back to DEFAULT_VIEW_ICONS once an explicit icon is set — it returns resolveIcon(view.icon), i.e. null:

function getViewIcon(view: ViewSwitcherItem): LucideIcon | null {
  if (view.icon) {
    return resolveIcon(view.icon);
  }
  return DEFAULT_VIEW_ICONS[view.type] || null;
}

and the render site is {Icon ? ( Icon ) : null}. ObjectView always supplies an icon string (icon: iconMap[v.type] || 'table'), so a host-composed chart or gantt view is drawn as a label with no icon at all, while every other type in the same switcher has one. No crash. The console reaches this: CreateViewDialog offers both chart and gantt among the view types a user can create, and those records arrive as ObjectView's views prop.

Why it is worth a card rather than a drive-by

Picking the replacements is a judgement call, not a mechanical rename — lucide offers several candidates per glyph (ChartColumn, ChartColumnBig, ChartGantt, ChartNoAxesGantt), and the choice should probably be made together with the DEFAULT_VIEW_ICONS entries beside them, which still import the deprecated aliases. There is also a guard question worth settling in the same breath: nothing today fails when a name stops resolving, which is exactly why this survived a major lucide bump. A pin asserting that every name the map supplies resolves in icons would close the class; #5321 deliberately left its resolvability assertion scoped to tree alone and says so in a comment, so that the widening happens here with the fix rather than as a surprise red.

Not fixed in #5321

#5321's ruling was about which view types are authorable, and its one behaviour change was adding the missing tree KEY (tree: 'list-tree', verified resolving). Its scope fence is packages/plugin-view/src/** plus the record; changing two working-looking icon names on a judgement call is outside it.

Dedup

Searched open issues for the lucide-name shape, for the iconMap / view-switcher icon shape, and for lucide upgrade cards. Nothing open covers it. Related and closed: #2916 (added the chart key to this same map — the key it added is the one now failing to resolve), #1885 (tree view added). Related and open: #5321 (the missing tree key, and the host-only view-type record).

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  2. added theissue type on Aug 21, 2026
  3. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage (first-touch grading): promoted finding → pm:queue, domain:ui + bug, type Bug. Rationale: measured user-visible defect on a console-reachable path (CreateViewDialog offers chart and gantt; both render iconless while every sibling has an icon), with the root cause isolated to lucide dropping the old spellings from the icons record while keeping deprecated named exports — which is also why the compiler stays green and why no gate caught the bump. The icon-name choice is dev judgment (pick from the card's candidate table together with the adjacent DEFAULT_VIEW_ICONS entries); the load-bearing half of the fix is the resolvability pin widened to every name the map supplies, which #5321 deliberately scoped to tree alone so the widening could land here — that pin closes the class against the next lucide bump.

    Dispatch notes: same file surface as in-flight #5321 (packages/plugin-view/src/**, same iconMap) — hard serial after #5321 lands; fold-or-serial answer owed by the dispatching seat if #5321 is still open at claim time. Size/model suggestion: S–M.


    Generated by Claude Code

  4. self-assigned this
    on Aug 21, 2026
  5. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor

    Claim — domain:ui execution seat (#5560), round 8.

    • Session: session_012u2pRjcqAYtoEjgr3wwhnK
    • Branch: claude/issue-5586-viewswitcher-chart-gantt-icons
    • Label: pm:queue → pm:dispatched

    Dispatch notes honoured from triage: the serialization concern against #5321 is cleared (that work has landed), and the load-bearing half of the fix is taken to be the resolvability pin widened to every name DEFAULT_VIEW_ICONS supplies — not just chart/gantt. Icon selection comes from the card's own verified-resolving candidates.


    Generated by Claude Code

  6. added a commit that references this issue on Aug 21, 2026
    454a8c0
  7. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 5586,
      "status": "done",
      "premise_still_valid": true,
      "branch": "claude/issue-5586-viewswitcher-chart-gantt-icons",
      "pr_url": "https://github.com/objectstack-ai/objectui/pull/5626",
      "head_sha": "550f4c902",
      "files_changed": [
        "packages/plugin-view/src/ObjectView.tsx",
        "packages/plugin-view/src/ViewSwitcher.tsx",
        "packages/plugin-view/src/__tests__/ViewSwitcher.test.tsx",
        "packages/plugin-view/src/__tests__/ObjectView.hostOnlyViewTypes.test.tsx",
        ".changeset/view-switcher-chart-gantt-icons-5586.md"
      ],
      "what_changed": "ObjectView's iconMap: chart 'bar-chart-3' -> 'chart-column', gantt 'gantt-chart' -> 'chart-gantt'. ViewSwitcher's DEFAULT_VIEW_ICONS de-aliased in the same pass: BarChart3 -> ChartColumn, Grid -> Grid3x3 (both are the SAME component under a live name, so those glyphs are unchanged), GanttChartSquare -> ChartGantt (the one visual change: square-framed SquareChartGantt -> plain ChartGantt, so the default agrees with the string iconMap now supplies for that view type). The resolvability pin widens from `tree` alone to EVERY name both maps supply, hosted in ViewSwitcher.test.tsx (where the real component renders the names) rather than at the old do-not-widen marker in ObjectView.hostOnlyViewTypes.test.tsx, whose comment is updated to point at it - that file mocks ../ViewSwitcher away, so a pin there could only re-implement toPascalCase + the record lookup by hand, a mirror that can drift while staying green. Two probes because they fail differently: the STRING map is pinned by rendering every name and asserting an svg; the COMPONENT map is pinned by icons-record membership (a retired alias keeps rendering there, so nothing else would go red). Each has a control at identical scope, and a precondition test proves the source read found exactly ViewType's members so nothing is vacuously green.",
      "evidence": {
        "lucide_version": "lucide-react@1.31.0, read from the resolved package: node -p \"require('lucide-react/package.json').version\" -> 1.31.0 at node_modules/.pnpm/lucide-react@1.31.0_react@19.2.8/node_modules/lucide-react. The `icons` record has 1767 keys.",
        "icon_resolution_probe": "BEFORE (iconMap name -> icons record): gantt 'gantt-chart' -> GanttChart *** MISSING ***; chart 'bar-chart-3' -> BarChart3 *** MISSING ***; the other nine (kanban/calendar/map/gallery/timeline/grid/list/detail/tree) all RESOLVE. REPLACEMENTS re-verified on the installed version: chart-column -> ChartColumn inIcons=true; chart-gantt -> ChartGantt inIcons=true. DEFAULT_VIEW_ICONS named imports: BarChart3 inIcons=false (displayName ChartColumn), GanttChartSquare inIcons=false (displayName SquareChartGantt), Grid inIcons=false (displayName Grid3x3); the other 13 all inIcons=true. Alias identity: BarChart3 === ChartColumn true, Grid === Grid3x3 true, GanttChartSquare === ChartGantt false. ABLATION (revert only the two src files to origin/main, mutation confirmed on disk by anchored grep counts - 'base values back=2 fixed values gone=0 | base idents back=3 fixed idents gone=0' - restored by trap ... EXIT INT TERM): 2 failed | 6 passed, naming '`gantt: gantt-chart` renders NO icon' and [['grid','Grid'],['gantt','GanttChartSquare'],['chart','BarChart3']]. No build needed - the suite imports ../ObjectView and ../ViewSwitcher as source and the root Vitest config aliases @object-ui/* to sibling src/, so nothing resolves through dist/. Restore leg re-measured green 15/15.",
        "dist_deltas": "Both legs built with `pnpm --filter @object-ui/plugin-view build`, sizes read at the real packages/plugin-view/dist path (no copies, no gzip): index.js base=86530 fix=86528 delta=-2; index.umd.cjs base=68095 fix=68095 delta=0 but NOT identical (cmp: differs at char 2083); index.d.ts 1402/1402 byte-identical (diff -u empty); ObjectView.d.ts 16503, ViewSwitcher.d.ts 641, and every other .d.ts delta=0. Content check: bar-chart-3 1->0 and chart-column 0->1 in BOTH index.js and index.umd.cjs. So zero .d.ts movement with real movement in the emitted JS - the mirror image of the prose-card criterion, exactly as the PM's mid-flight correction predicted."
      },
      "changeset": {
        "present": true,
        "file": ".changeset/view-switcher-chart-gantt-icons-5586.md",
        "bump": "patch",
        "why": "Owed by the authority named in the PM correction, not by the .d.ts heuristic. `node scripts/check-changeset-presence.mjs` verdict: '4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/view-switcher-chart-gantt-icons-5586.md.' (exit 0). Independently corroborated by the measurement: dist/index.js and dist/index.umd.cjs both carry the new icon names, so shipped runtime values move even though every .d.ts is byte-identical. patch, not minor/major - a bug fix, and the repo forbids major in the fixed group."
      },
      "tests": {
        "command": "pnpm exec vitest run packages/plugin-view/src/__tests__/ViewSwitcher.test.tsx packages/plugin-view/src/__tests__/ObjectView.hostOnlyViewTypes.test.tsx  AND  pnpm exec vitest run packages/plugin-view  (both from the REPO ROOT - package-cwd vitest is refused by the repo's own guard; both wrapped in the container's shared heavy-verify lock)",
        "result": "targeted: 'Test Files 2 passed (2)' / 'Tests 15 passed (15)'. whole package: 'Test Files 19 passed (19)' / 'Tests 199 passed (199)'. Targeted run re-measured at the final commit 550f4c902 after the changeset commit: same verdict."
      },
      "gates": [
        { "name": "vitest (2 targeted files, repo root)", "status": "pass - Test Files 2 passed (2), Tests 15 passed (15) @ 550f4c902" },
        { "name": "vitest (whole plugin-view package, repo root)", "status": "pass - Test Files 19 passed (19), Tests 199 passed (199)" },
        { "name": "pnpm --filter @object-ui/plugin-view type-check (tsc --noEmit; tsc -p tsconfig.test.json)", "status": "pass - exit 0, run after building the dependency closure with pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-view^...' build" },
        { "name": "pnpm --filter @object-ui/plugin-view lint", "status": "pass - exit 0, verdict line 'x 229 problems (0 errors, 229 warnings)'; the warnings are the package's pre-existing debt and the count is unchanged" },
        { "name": "node scripts/check-changeset-presence.mjs", "status": "pass - exit 0, '4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'" },
        { "name": "pnpm changeset:check (check-changeset-fixed + check-changeset-no-major)", "status": "pass - exit 0, 'All workspace packages are in the changeset fixed group.' / 'No changeset declares a major bump.'" },
        { "name": "node scripts/check-control-bytes.mjs", "status": "pass - exit 0, 'OK (scanned 4672 tracked text file(s); skipped 85 binary)'; plus a manual grep -naP scan of the four edited files for control bytes, no hits" },
        { "name": "check-eager-closure-budget / check-doc-snippet-types", "status": "not run - the two known-broken gauges named in the dispatch (they exit non-zero until a console build / workspace build writes their inputs); not 'fixed', not claimed green" },
        { "name": "CI (full farm)", "status": "in_progress - report delivered at draft-PR time per the standing contract; CI convergence is the reviewing seat's read" }
      ],
      "scope_fences_respected": true,
      "notes_for_review": [
        "PIN PLACEMENT is the one deliberate deviation from the literal dispatch wording: the widened pin lives in ViewSwitcher.test.tsx, not at the do-not-widen marker in ObjectView.hostOnlyViewTypes.test.tsx. That file mocks ../ViewSwitcher, so a pin there would have to hand-copy toPascalCase + the icons lookup - a mirror that can drift out from under the assertion while staying green. The marker's comment is rewritten to point at the new home. Reject and I will move it.",
        "ONE BOUNDED IN-PLACE FIX, declared: ViewSwitcher.test.tsx's 'explicit label and icon override' fixture read icon: 'pie-chart'. PieChart is a retired alias (canonical ChartPie, inIcons=false), so that fixture rendered a label and NO icon and stayed green because it only ever asserted the label. Same defect class, same file, mechanically determined by the same probe: it now reads 'chart-pie' AND asserts the icon renders.",
        "LINT NARROWING, declared: the repo-wide pnpm lint was narrowed to the affected package. Universe = eslint's own resolution of '.' from the package directory, which is exactly what the per-package lint task runs; 34 files, count read from --format json output; 0 errors. Invariance: eslint.config.js declares no type-aware parser options (projectService / project: / parserOptions / tsconfigRootDir all absent, recommendedTypeChecked count 0), so this diff cannot move the verdict on any file it does not touch. CI runs the full farm regardless.",
        "The PR body's attribution footer uses the session-URL form (_Generated by [Claude Code](https://claude.ai/code/session_...)_) rather than the bare form: the bare form is stripped from a PR body on every subsequent edit, and this body was edited once already (see next note). Same footer text, link carries the session.",
        "CORRECTED MEASUREMENT - NOT a GitHub sanitizer event. The first version of the PR body read back as though two angle-bracket fragments had been eaten (Record<ViewType, ...> and icon: '<kebab>'), so they were rewritten without angle brackets. Reading both bodies back through the REST API afterwards shows GitHub stored every fragment INTACT: the elision happens in the MCP issue/PR read path used to verify, which strips angle-bracket fragments from the body it returns. Same path hides the HTML-comment report marker, which is why this comment now also leads with the literal text os-dev-report. The PR-body rewrite is left in place (equally accurate prose) and its own note corrected."
      ],
      "out_of_scope_findings": [
        "filed as #5622: two more retired lucide spellings reach the icons-record resolver on live paths outside plugin-view - icon: 'edit' in plugin-detail/src/DetailView.tsx:784 (the mobile Edit action, rendered through an action:bar and components' resolve-icon.ts; Edit is absent from the record, canonical square-pen) and icon: 'smile' in components/src/renderers/basic/icon.tsx:83 (the icon renderer's OWN registry default and its name input's defaultValue; Smile is absent, canonical face-slightly-smiling). Also recorded there: plugin-list/src/ViewSwitcher.tsx names the same three stale component aliases (Grid, GanttChartSquare, BarChart3); four hand-copied resolver implementations read the icons record while app-shell's getIcon and layout's resolveIcon read lucide/dynamic's iconNames (2025 entries, which DOES carry the retired aliases), so the same name renders in the sidebar and renders nothing in an action bar; and only plugin-view now has a resolvability pin. Filed unassigned, no labels, for triage."
      ],
      "open_questions": []
    }

    Generated by Claude Code

  8. added a commit that references this issue on Aug 23, 2026
    6b348d2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatplugin-viewpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions