mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 17:28:03 +08:00
refactor: extract FieldHelp and MenuItem settings sub-components (#1402)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user