{"record":{"id":"e02f31154494306b","repo":"spree/spree","slug":"nav-entry-key-not-found","errorCode":null,"errorMessage":"Nav entry \"${key}\" not found.","messagePattern":"Nav entry \"(.+?)\" not found\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/dashboard-core/src/lib/nav-registry.ts","lineNumber":120,"sourceCode":"// ============================================================================\n// Public API\n// ============================================================================\n\nexport const nav: NavMutator = {\n  add(entry) {\n    ensureUniqueKey(entry.key)\n    entries.push(entry)\n    notify()\n  },\n  remove(key) {\n    const i = findIndex(key)\n    if (i === -1) return\n    entries.splice(i, 1)\n    notify()\n  },\n  update(key, patch) {\n    const e = entries.find((x) => x.key === key)\n    if (!e) throw new Error(`Nav entry \"${key}\" not found.`)\n    Object.assign(e, patch)\n    notify()\n  },\n  insertBefore(targetKey, entry) {\n    const i = findIndex(targetKey)\n    if (i === -1) throw new Error(`Nav entry \"${targetKey}\" not found.`)\n    ensureUniqueKey(entry.key)\n    // Inherit target's position so the relative order survives `getNavEntries`'s sort.\n    const target = entries[i]\n    const adjusted: NavEntry = {\n      ...entry,\n      position: entry.position ?? (target.position ?? 100) - 1,\n    }\n    entries.splice(i, 0, adjusted)\n    notify()\n  },\n  insertAfter(targetKey, entry) {\n    const i = findIndex(targetKey)","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/spree/spree/blob/06bf66a8684b9de03210bbb2ddc8c1f5ba522fa2/packages/dashboard-core/src/lib/nav-registry.ts#L102-L138","documentation":"nav.update(key, patch) requires an existing top-level entry; when no registered entry matches the key it throws before mutating anything. The registry fails loudly rather than no-op'ing so a typo'd or stale key surfaces immediately, and the entry list stays consistent for listeners.","triggerScenarios":"update('product', ...) misspelling the built-in 'products'; the target entry removed by another plugin before yours updates; a key renamed in a newer dashboard version; plugin init running before built-ins register.","commonSituations":"Key copied from outdated docs or a different dashboard version; load-order changes after a plugin update; another plugin conditionally removing the section you patch.","solutions":["Read the exact keys from the current nav snapshot (getNavEntries) and match the casing","Defer the update until after app boot when patching built-ins, so they have registered first","Guard the update with an existence check and fall back to add() when the entry is genuinely absent"],"exampleFix":"// before\nnav.update('product', { label: 'Catalog' }) // throws: not found\n\n// after\nnav.update('products', { label: 'Catalog' })","handlingStrategy":"validation","validationCode":"const exists = getNavEntries().main.some((e) => e.key === 'products')\nif (exists) {\n  nav.update('products', patch)\n} else {\n  nav.add({ key: 'products', ...patch })\n}","typeGuard":null,"tryCatchPattern":"try {\n  nav.update(key, patch)\n} catch (err) {\n  if (err instanceof Error && err.message.includes('not found')) {\n    nav.add({ key, ...patch })\n  } else {\n    throw err\n  }\n}","preventionTips":["Derive keys from getNavEntries() rather than hardcoding","Defer built-in patching to post-boot hooks","Pin plugin versions against dashboards whose nav keys you've verified","Type keys as literal unions to catch typos at compile time"],"tags":["registry","navigation","plugin-api","unknown-key","dashboard-core"],"backgroundTag":"registry-key-not-found","analyzedSha":"06bf66a8684b9de03210bbb2ddc8c1f5ba522fa2","analyzedAt":"2026-08-21T14:59:48.125Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}