refactor: extract FieldHelp and MenuItem settings sub-components (#1402)

This commit is contained in:
Hiep Le
2026-06-17 15:49:38 +00:00
committed by GitHub
parent d4dba6aa59
commit 809c17a5fd
4 changed files with 117 additions and 112 deletions
@@ -0,0 +1,46 @@
import { cn } from "#/utils/utils";
import { ConversationNameContextMenuIconText } from "#/components/features/conversation/conversation-name-context-menu-icon-text";
interface MenuItemProps {
index: number;
icon: React.ReactNode;
label: string;
onClick: () => void;
onKeyDown: (e: React.KeyboardEvent, index: number) => void;
menuItemsRef: React.MutableRefObject<(HTMLButtonElement | null)[]>;
disabled?: boolean;
testId: string;
}
export function MenuItem({
index,
icon,
label,
onClick,
onKeyDown,
menuItemsRef,
disabled,
testId,
}: MenuItemProps) {
return (
<button
ref={(el) => {
// eslint-disable-next-line no-param-reassign
menuItemsRef.current[index] = el;
}}
type="button"
onClick={onClick}
onKeyDown={(e) => onKeyDown(e, index)}
disabled={disabled}
className={cn(
"group w-full cursor-pointer rounded px-2 py-2 text-start text-nowrap text-sm font-normal",
"text-[var(--oh-foreground)] hover:bg-[var(--oh-interactive-hover)]",
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent",
)}
role="menuitem"
data-testid={testId}
>
<ConversationNameContextMenuIconText icon={icon} text={label} />
</button>
);
}
@@ -11,54 +11,10 @@ import { TextCursor, Copy } from "lucide-react";
import { cn } from "#/utils/utils";
import { dropdownMenuListClassName } from "#/utils/dropdown-classes";
import { I18nKey } from "#/i18n/declaration";
import { ConversationNameContextMenuIconText } from "#/components/features/conversation/conversation-name-context-menu-icon-text";
import EditIcon from "#/icons/u-edit.svg?react";
import CheckCircleIcon from "#/icons/u-check-circle.svg?react";
import DeleteIcon from "#/icons/u-delete.svg?react";
interface MenuItemProps {
index: number;
icon: React.ReactNode;
label: string;
onClick: () => void;
onKeyDown: (e: React.KeyboardEvent, index: number) => void;
menuItemsRef: React.MutableRefObject<(HTMLButtonElement | null)[]>;
disabled?: boolean;
testId: string;
}
function MenuItem({
index,
icon,
label,
onClick,
onKeyDown,
menuItemsRef,
disabled,
testId,
}: MenuItemProps) {
return (
<button
ref={(el) => {
// eslint-disable-next-line no-param-reassign
menuItemsRef.current[index] = el;
}}
type="button"
onClick={onClick}
onKeyDown={(e) => onKeyDown(e, index)}
disabled={disabled}
className={cn(
"group w-full cursor-pointer rounded px-2 py-2 text-start text-nowrap text-sm font-normal",
"text-[var(--oh-foreground)] hover:bg-[var(--oh-interactive-hover)]",
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent",
)}
role="menuitem"
data-testid={testId}
>
<ConversationNameContextMenuIconText icon={icon} text={label} />
</button>
);
}
import { MenuItem } from "./profile-actions-menu-item";
interface ProfileActionsMenuProps {
onEdit: () => void;
@@ -0,0 +1,69 @@
import { useTranslation } from "react-i18next";
import { SettingsFieldSchema } from "#/types/settings";
import { HelpLink } from "#/ui/help-link";
import { Typography } from "#/ui/typography";
import { resolveSchemaFieldDescription } from "#/utils/sdk-settings-field-metadata";
// ---------------------------------------------------------------------------
// Help links – UI-only mapping from field keys to user-facing guidance.
// Keys use conventional i18n pattern: SCHEMA$<PATH>$HELP_TEXT / HELP_LINK_TEXT
// ---------------------------------------------------------------------------
export const FIELD_HELP_LINKS: Record<
string,
{
textKey: string;
linkTextKey: string;
href: string;
/** Skip rendering the schema description separately when the help text already includes it. */
hideDescription?: boolean;
/** Optional trailing copy rendered after the link (e.g. " tab of OpenHands Cloud."). */
suffixKey?: string;
}
> = {
"llm.api_key": {
textKey: "SCHEMA$LLM$API_KEY$HELP_TEXT",
linkTextKey: "SCHEMA$LLM$API_KEY$HELP_LINK_TEXT",
href: "https://docs.openhands.dev/usage/local-setup#getting-an-api-key",
},
// Mirror the hint shown under the LLM provider's API key field when
// OpenHands is selected as the active provider; the SDK reuses that active
// LLM key when the critic key is empty.
"verification.critic_api_key": {
textKey: "SCHEMA$VERIFICATION$CRITIC_API_KEY$HELP_TEXT",
linkTextKey: "SETTINGS$NAV_API_KEYS",
suffixKey: "SCHEMA$VERIFICATION$CRITIC_API_KEY$HELP_SUFFIX",
href: "https://app.all-hands.dev/settings/api-keys",
hideDescription: true,
},
};
export function FieldHelp({ field }: { field: SettingsFieldSchema }) {
const { t } = useTranslation("openhands");
const helpLink = FIELD_HELP_LINKS[field.key];
const description = resolveSchemaFieldDescription(
t,
field.key,
field.description,
);
return (
<>
{description && !helpLink?.hideDescription ? (
<Typography.Paragraph className="text-tertiary-alt text-xs leading-5">
{description}
</Typography.Paragraph>
) : null}
{helpLink ? (
<HelpLink
testId={`help-link-${field.key}`}
text={t(helpLink.textKey)}
linkText={t(helpLink.linkTextKey)}
href={helpLink.href}
suffix={helpLink.suffixKey ? ` ${t(helpLink.suffixKey)}` : undefined}
size="settings"
linkColor="white"
/>
) : null}
</>
);
}
@@ -5,12 +5,9 @@ import { SettingsDropdownInput } from "#/components/features/settings/settings-d
import { SettingsInput } from "#/components/features/settings/settings-input";
import { SettingsSwitch } from "#/components/features/settings/settings-switch";
import { SettingsFieldSchema } from "#/types/settings";
import { HelpLink } from "#/ui/help-link";
import { Typography } from "#/ui/typography";
import {
getSettingsFieldConstraints,
resolveSchemaChoiceLabel,
resolveSchemaFieldDescription,
resolveSchemaFieldLabel,
} from "#/utils/sdk-settings-field-metadata";
import { cn } from "#/utils/utils";
@@ -18,39 +15,7 @@ import {
formControlMultilineFieldClassName,
formControlSwitchDescriptionClassName,
} from "#/utils/form-control-classes";
// ---------------------------------------------------------------------------
// Help links – UI-only mapping from field keys to user-facing guidance.
// Keys use conventional i18n pattern: SCHEMA$<PATH>$HELP_TEXT / HELP_LINK_TEXT
// ---------------------------------------------------------------------------
export const FIELD_HELP_LINKS: Record<
string,
{
textKey: string;
linkTextKey: string;
href: string;
/** Skip rendering the schema description separately when the help text already includes it. */
hideDescription?: boolean;
/** Optional trailing copy rendered after the link (e.g. " tab of OpenHands Cloud."). */
suffixKey?: string;
}
> = {
"llm.api_key": {
textKey: "SCHEMA$LLM$API_KEY$HELP_TEXT",
linkTextKey: "SCHEMA$LLM$API_KEY$HELP_LINK_TEXT",
href: "https://docs.openhands.dev/usage/local-setup#getting-an-api-key",
},
// Mirror the hint shown under the LLM provider's API key field when
// OpenHands is selected as the active provider; the SDK reuses that active
// LLM key when the critic key is empty.
"verification.critic_api_key": {
textKey: "SCHEMA$VERIFICATION$CRITIC_API_KEY$HELP_TEXT",
linkTextKey: "SETTINGS$NAV_API_KEYS",
suffixKey: "SCHEMA$VERIFICATION$CRITIC_API_KEY$HELP_SUFFIX",
href: "https://app.all-hands.dev/settings/api-keys",
hideDescription: true,
},
};
import { FieldHelp } from "./field-help";
/**
* Field keys that should span the full settings grid (both columns on xl
@@ -62,37 +27,6 @@ export const FIELD_FULL_WIDTH_KEYS: ReadonlySet<string> = new Set([
"verification.critic_api_key",
]);
function FieldHelp({ field }: { field: SettingsFieldSchema }) {
const { t } = useTranslation("openhands");
const helpLink = FIELD_HELP_LINKS[field.key];
const description = resolveSchemaFieldDescription(
t,
field.key,
field.description,
);
return (
<>
{description && !helpLink?.hideDescription ? (
<Typography.Paragraph className="text-tertiary-alt text-xs leading-5">
{description}
</Typography.Paragraph>
) : null}
{helpLink ? (
<HelpLink
testId={`help-link-${field.key}`}
text={t(helpLink.textKey)}
linkText={t(helpLink.linkTextKey)}
href={helpLink.href}
suffix={helpLink.suffixKey ? ` ${t(helpLink.suffixKey)}` : undefined}
size="settings"
linkColor="white"
/>
) : null}
</>
);
}
function isSelectField(field: SettingsFieldSchema): boolean {
return field.choices.length > 0;
}