Files
kandev/apps/web/components/app-sidebar/sections/settings/settings-menu-node.tsx
T
2026-08-11 19:34:52 +02:00

137 lines
4.3 KiB
TypeScript

"use client";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { AgentLogo } from "@/components/agent-logo";
import {
ActiveWorkspaceBadge,
DisabledBadge,
NotInstalledBadge,
} from "@/components/settings/record-badges";
import type { SettingsMenuNode } from "./settings-menu-branches";
import { IntegrationEnabledBadgeFor, IntegrationsEnabledProvider } from "./integration-enabled";
import { RecordGlyph, SettingsBranch, SettingsLeaf } from "./settings-nav-primitives";
import type { SettingsMenuExpansion } from "./use-settings-menu-expansion";
type SettingsMenuNodeRowProps = {
node: SettingsMenuNode;
/** Nesting level. Menu rows are 0, so a branch's own children start at 1. */
depth: number;
/**
* The single node allowed to mark itself active — the deepest one owning the
* current route. Ancestors merely hold it open.
*/
activeKey: string | null;
expansion: SettingsMenuExpansion;
};
/**
* The row's leading visual: an agent's logo, the record dot, or nothing — in
* which case `NavRowLabel` keeps the glyph box open. A node that already
* declares an `icon` is left to it.
*/
function resolveLeadingIcon(node: SettingsMenuNode, isRecord: boolean) {
if (node.agentName) {
return <AgentLogo agentName={node.agentName} className="h-3.5 w-3.5 shrink-0" />;
}
if (isRecord && !node.icon) return <RecordGlyph />;
return undefined;
}
/** One badge per node at most; `undefined` is much the commonest case. */
function renderBadge(badge: SettingsMenuNode["badge"]) {
if (badge === "disabled") return <DisabledBadge />;
if (badge === "active") return <ActiveWorkspaceBadge />;
if (badge === "not-installed") return <NotInstalledBadge />;
return undefined;
}
/**
* Wraps the Integrations branch's rows in one shared probe. Sits here, inside
* the collapsible body, so a closed branch never fetches.
*/
function withIntegrationProbe(node: SettingsMenuNode, rows: ReactNode) {
if (!node.integrationsWorkspaceId) return rows;
return (
<IntegrationsEnabledProvider workspaceId={node.integrationsWorkspaceId}>
{rows}
</IntegrationsEnabledProvider>
);
}
/**
* One row of a settings menu branch, recursing into its children.
*
* A node with children renders as a disclosure; a node without renders as a
* plain link. A node with neither children nor an `href` renders nothing at
* all — it could not be navigated to or opened, so it would be a dead row.
*/
export function SettingsMenuNodeRow({
node,
depth,
activeKey,
expansion,
}: SettingsMenuNodeRowProps) {
const { t } = useTranslation();
const label = "key" in node.label ? t(node.label.key) : node.label.text;
const isRecord = node.isUserRecord === true;
// Records carry no glyph of their own, so the dot stands in as one. Supplied
// here rather than by the branch builders: it belongs to being a record, and
// stating it once beats repeating it in all three of them.
const leadingIcon = resolveLeadingIcon(node, isRecord);
const isActive = node.key === activeKey;
const children = node.children ?? [];
// An integration's badge needs a probe, so it resolves in the row rather than
// in the node; everything else is decided when the branch is built.
const trailing = node.integrationSlug ? (
<IntegrationEnabledBadgeFor slug={node.integrationSlug} />
) : (
renderBadge(node.badge)
);
if (children.length === 0) {
if (!node.href) return null;
return (
<SettingsLeaf
href={node.href}
label={label}
icon={node.icon}
leadingIcon={leadingIcon}
trailing={trailing}
isActive={isActive}
depth={depth}
isRecord={isRecord}
/>
);
}
return (
<SettingsBranch
label={label}
icon={node.icon}
leadingIcon={leadingIcon}
trailing={trailing}
href={node.href ?? undefined}
isActive={isActive}
expanded={expansion.isExpanded(node.key)}
onToggle={() => expansion.toggle(node.key)}
depth={depth}
isRecord={isRecord}
>
{withIntegrationProbe(
node,
children.map((child) => (
<SettingsMenuNodeRow
key={child.key}
node={child}
depth={depth + 1}
activeKey={activeKey}
expansion={expansion}
/>
)),
)}
</SettingsBranch>
);
}