From 8d9a6d85e324ad54002e2dc331e4870b94deaf2a Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Wed, 15 Jul 2026 12:12:21 +0800 Subject: [PATCH] fix(ui): render select labels instead of values --- packages/ui/components/ui/select.tsx | 17 ++++++++++- .../components/autopilot-dialog.tsx | 22 ++++++++++---- .../autopilots/components/trigger-config.tsx | 4 +++ packages/views/common/select.test.tsx | 29 +++++++++++++++++++ packages/views/common/timezone-select.tsx | 8 +++-- .../dashboard/components/dashboard-page.tsx | 5 ++++ .../components/cloud-runtime-dialog.tsx | 6 +++- .../views/settings/components/members-tab.tsx | 9 +++++- .../settings/components/preferences-tab.tsx | 9 ++++++ .../settings/components/properties-tab.tsx | 11 +++++-- .../views/settings/components/tokens-tab.tsx | 14 +++++++-- .../runtime-local-skill-import-panel.tsx | 4 +++ 12 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 packages/views/common/select.test.tsx diff --git a/packages/ui/components/ui/select.tsx b/packages/ui/components/ui/select.tsx index 08257e09bf..c3cb5e45ba 100644 --- a/packages/ui/components/ui/select.tsx +++ b/packages/ui/components/ui/select.tsx @@ -6,7 +6,22 @@ import { Select as SelectPrimitive } from "@base-ui/react/select" import { cn } from "@multica/ui/lib/utils" import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" -const Select = SelectPrimitive.Root +type SelectProps< + Value, + Multiple extends boolean | undefined = false, +> = SelectPrimitive.Root.Props & { + items: NonNullable["items"]> +} + +/** + * Base UI renders the raw value unless Root receives an items label map. + * Keep items required so every select has a single source for value labels. + */ +function Select( + props: SelectProps +) { + return +} function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { return ( diff --git a/packages/views/autopilots/components/autopilot-dialog.tsx b/packages/views/autopilots/components/autopilot-dialog.tsx index c9ae0a97df..c89e5c13f9 100644 --- a/packages/views/autopilots/components/autopilot-dialog.tsx +++ b/packages/views/autopilots/components/autopilot-dialog.tsx @@ -963,6 +963,14 @@ function ScheduleSection({ const formatCountdown = useFormatCountdown(); const now = useNowTicker(); const next = useMemo(() => computeNextRun(config, now), [config, now]); + const frequencyItems = FREQUENCY_KEYS.map((value) => ({ + value, + label: t(($) => $.dialog.frequency_long[value]), + })); + const dayItems = DAY_KEYS.map((dayKey, value) => ({ + value: String(value), + label: t(($) => $.dialog.days[dayKey]), + })); const timezones = useMemo(() => { const local = getLocalTimezone(); if (TIMEZONE_OPTIONS.includes(local)) return TIMEZONE_OPTIONS; @@ -983,6 +991,7 @@ function ScheduleSection({ {/* Row 1: Frequency + (Day when weekly) */}
{config.frequency === "weekly" ? ( ({ + value, + label: getTimezoneLabel(value), + }))} value={config.timezone} onValueChange={(v) => v && onChange({ ...config, timezone: v })} > diff --git a/packages/views/common/select.test.tsx b/packages/views/common/select.test.tsx new file mode 100644 index 0000000000..59f738b750 --- /dev/null +++ b/packages/views/common/select.test.tsx @@ -0,0 +1,29 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { + Select, + SelectTrigger, + SelectValue, +} from "@multica/ui/components/ui/select"; + +describe("Select", () => { + it("renders the selected item label instead of its raw value", () => { + render( + , + ); + + const trigger = screen.getByRole("combobox", { name: "Property type" }); + expect(trigger).toHaveTextContent("Multi-select"); + expect(trigger).not.toHaveTextContent("multi_select"); + }); +}); diff --git a/packages/views/common/timezone-select.tsx b/packages/views/common/timezone-select.tsx index cf25170646..4f5e83eb01 100644 --- a/packages/views/common/timezone-select.tsx +++ b/packages/views/common/timezone-select.tsx @@ -93,9 +93,11 @@ export function TimezoneSelect({ const options = timezoneOptions(value); const render = (tz: string) => tz === browser ? `${tz}${browserSuffix}` : tz; + const items = options.map((value) => ({ value, label: render(value) })); return ( onChange(v ?? ALL_PROJECTS)} > diff --git a/packages/views/runtimes/components/cloud-runtime-dialog.tsx b/packages/views/runtimes/components/cloud-runtime-dialog.tsx index 31367b8e7a..f71635bceb 100644 --- a/packages/views/runtimes/components/cloud-runtime-dialog.tsx +++ b/packages/views/runtimes/components/cloud-runtime-dialog.tsx @@ -254,7 +254,11 @@ function LabeledInput({ - ({ value: option, label: option }))} + value={value} + onValueChange={(next) => onChange(next ?? value)} + > {() => {value}} diff --git a/packages/views/settings/components/members-tab.tsx b/packages/views/settings/components/members-tab.tsx index c7232f0bbf..7659e4c2c0 100644 --- a/packages/views/settings/components/members-tab.tsx +++ b/packages/views/settings/components/members-tab.tsx @@ -356,7 +356,14 @@ export function MembersTab() { if (e.key === "Enter" && inviteEmail.trim()) handleInviteMember(); }} /> - ({ + value, + label: roleConfig[value].label, + }))} + value={inviteRole} + onValueChange={(value) => setInviteRole(value as MemberRole)} + > {() => roleConfig[inviteRole].label} diff --git a/packages/views/settings/components/preferences-tab.tsx b/packages/views/settings/components/preferences-tab.tsx index 2bc8331616..27f3465256 100644 --- a/packages/views/settings/components/preferences-tab.tsx +++ b/packages/views/settings/components/preferences-tab.tsx @@ -101,6 +101,7 @@ export function PreferencesTab() { size="select" > { if (next) void handleLanguageChange(next as SupportedLocale); @@ -243,6 +245,13 @@ function TimezoneRow() { size="select-wide" > value && @@ -492,9 +497,9 @@ function PropertyEditorDialog({ - {ISSUE_PROPERTY_TYPES.map((type) => ( - - + {propertyTypeItems.map((item) => ( + + {item.label} ))} diff --git a/packages/views/settings/components/tokens-tab.tsx b/packages/views/settings/components/tokens-tab.tsx index b3a129d4a3..4787557bd6 100644 --- a/packages/views/settings/components/tokens-tab.tsx +++ b/packages/views/settings/components/tokens-tab.tsx @@ -44,6 +44,10 @@ const EXPIRY_KEYS = ["30", "90", "365", "never"] as const; export function TokensTab() { const { t } = useT("settings"); + const expiryItems = EXPIRY_KEYS.map((value) => ({ + value, + label: t(($) => $.tokens.expiry[value]), + })); const [tokens, setTokens] = useState([]); const [tokenName, setTokenName] = useState(""); const [tokenExpiry, setTokenExpiry] = useState("90"); @@ -147,14 +151,18 @@ export function TokensTab() { onChange={(e) => setTokenName(e.target.value)} placeholder={t(($) => $.tokens.name_placeholder)} /> - { if (v) setTokenExpiry(v); }} + > $.tokens.title)} > - {EXPIRY_KEYS.map((key) => ( - {t(($) => $.tokens.expiry[key])} + {expiryItems.map((item) => ( + {item.label} ))} diff --git a/packages/views/skills/components/runtime-local-skill-import-panel.tsx b/packages/views/skills/components/runtime-local-skill-import-panel.tsx index 259e82ff92..59977960c9 100644 --- a/packages/views/skills/components/runtime-local-skill-import-panel.tsx +++ b/packages/views/skills/components/runtime-local-skill-import-panel.tsx @@ -1162,6 +1162,10 @@ export function RuntimeLocalSkillImportPanel({ {t(($) => $.runtime_import.runtime_label)}