Files
fusion/packages/dashboard/app/components/TrackingRepoSelect.tsx
Fusion (runfusion.ai) 51c60fe1b5 feat(FN-4731): add tracking repo dropdown to settings modal
Adds a `TrackingRepoSelect` dropdown component to the SettingsModal (with co-located CSS and full test coverage), updates the settings reference docs, and includes the FN-4731 changeset for the `@runfusion/fusion` package.

Fusion-Task-Id: FN-4731
2026-05-16 09:20:33 -07:00

115 lines
3.2 KiB
TypeScript

import { useEffect, useId, useMemo, useRef, useState } from "react";
import "./TrackingRepoSelect.css";
const CUSTOM_VALUE = "__custom__";
export interface TrackingRepoOption {
value: string;
label: string;
source?: string;
}
interface TrackingRepoSelectProps {
id: string;
value: string;
onChange: (value: string) => void;
options: TrackingRepoOption[];
loading?: boolean;
error?: string;
placeholder?: string;
ariaLabel?: string;
}
function normalizeOptions(options: TrackingRepoOption[]): TrackingRepoOption[] {
const byValue = new Map<string, TrackingRepoOption>();
for (const option of options) {
const normalized = option.value.trim();
if (!normalized || byValue.has(normalized)) {
continue;
}
byValue.set(normalized, {
...option,
value: normalized,
label: option.label?.trim() || normalized,
});
}
return [...byValue.values()].sort((a, b) => a.value.localeCompare(b.value));
}
export function TrackingRepoSelect({
id,
value,
onChange,
options,
loading = false,
error,
placeholder = "owner/repo",
ariaLabel,
}: TrackingRepoSelectProps) {
const customInputRef = useRef<HTMLInputElement>(null);
const hintId = useId();
const [showCustomInput, setShowCustomInput] = useState(false);
const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
const selectedOption = normalizedOptions.find((option) => option.value === value);
const selectValue = selectedOption ? selectedOption.value : CUSTOM_VALUE;
useEffect(() => {
setShowCustomInput(selectValue === CUSTOM_VALUE);
}, [selectValue]);
const handleSelectChange = (nextValue: string) => {
if (nextValue === CUSTOM_VALUE) {
setShowCustomInput(true);
requestAnimationFrame(() => {
customInputRef.current?.focus();
});
return;
}
setShowCustomInput(false);
onChange(nextValue);
};
const renderedHint = error ? error : loading ? "Loading detected GitHub remotes…" : null;
return (
<div className="tracking-repo-select">
<select
id={id}
className="select"
value={selectValue}
onChange={(event) => handleSelectChange(event.target.value)}
aria-label={ariaLabel}
aria-describedby={renderedHint ? hintId : undefined}
>
{normalizedOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.source ? `${option.label}${option.source}` : option.label}
</option>
))}
<option value={CUSTOM_VALUE}>Custom</option>
</select>
{showCustomInput ? (
<input
ref={customInputRef}
className="input tracking-repo-select__custom-input"
type="text"
value={value}
placeholder={placeholder}
onChange={(event) => onChange(event.target.value)}
aria-describedby={renderedHint ? hintId : undefined}
/>
) : null}
{renderedHint ? (
<small
id={hintId}
className={error ? "tracking-repo-select__hint tracking-repo-select__hint--error" : "tracking-repo-select__hint"}
>
{renderedHint}
</small>
) : null}
</div>
);
}