Files
kandev/apps/web/components/task-create-dialog-selectors.tsx
T

316 lines
8.0 KiB
TypeScript

"use client";
import { useEffect, useRef, useState, memo, useCallback } from "react";
import { Textarea } from "@kandev/ui/textarea";
import { Combobox } from "./combobox";
const CURSOR_POINTER_CLASS = "cursor-pointer";
type RepositoryOption = {
value: string;
label: string;
renderLabel: () => React.ReactNode;
};
type RepositorySelectorProps = {
options: RepositoryOption[];
value: string;
onValueChange: (value: string) => void;
disabled: boolean;
placeholder: string;
searchPlaceholder: string;
emptyMessage: string;
triggerClassName?: string;
};
export const RepositorySelector = memo(function RepositorySelector({
options,
value,
onValueChange,
disabled,
placeholder,
searchPlaceholder,
emptyMessage,
triggerClassName,
}: RepositorySelectorProps) {
return (
<Combobox
options={options}
value={value}
onValueChange={onValueChange}
placeholder={placeholder}
searchPlaceholder={searchPlaceholder}
emptyMessage={emptyMessage}
disabled={disabled}
dropdownLabel="Repository"
className={disabled ? undefined : CURSOR_POINTER_CLASS}
triggerClassName={triggerClassName}
testId="repository-selector"
/>
);
});
type BranchOption = {
value: string;
label: string;
renderLabel: () => React.ReactNode;
};
type BranchSelectorProps = {
options: BranchOption[];
value: string;
onValueChange: (value: string) => void;
disabled: boolean;
placeholder: string;
searchPlaceholder: string;
emptyMessage: string;
};
export const BranchSelector = memo(function BranchSelector({
options,
value,
onValueChange,
disabled,
placeholder,
searchPlaceholder,
emptyMessage,
}: BranchSelectorProps) {
return (
<Combobox
options={options}
value={value}
onValueChange={onValueChange}
placeholder={placeholder}
searchPlaceholder={searchPlaceholder}
emptyMessage={emptyMessage}
disabled={disabled}
dropdownLabel="Base Branch"
className={disabled ? undefined : CURSOR_POINTER_CLASS}
testId="branch-selector"
/>
);
});
type AgentSelectorProps = {
options: Array<{ value: string; label: string; renderLabel: () => React.ReactNode }>;
value: string;
onValueChange: (value: string) => void;
disabled: boolean;
placeholder: string;
triggerClassName?: string;
};
export const AgentSelector = memo(function AgentSelector({
options,
value,
onValueChange,
disabled,
placeholder,
triggerClassName,
}: AgentSelectorProps) {
return (
<Combobox
options={options}
value={value}
onValueChange={onValueChange}
placeholder={placeholder}
searchPlaceholder="Search agents..."
emptyMessage="No agent found."
disabled={disabled}
dropdownLabel="Agent profile"
className={`min-w-[380px]${disabled ? "" : ` ${CURSOR_POINTER_CLASS}`}`}
triggerClassName={triggerClassName}
testId="agent-profile-selector"
/>
);
});
type ExecutorSelectorProps = {
options: Array<{ value: string; label: string; renderLabel?: () => React.ReactNode }>;
value: string;
onValueChange: (value: string) => void;
disabled: boolean;
placeholder: string;
triggerClassName?: string;
};
export const ExecutorSelector = memo(function ExecutorSelector({
options,
value,
onValueChange,
disabled,
placeholder,
triggerClassName,
}: ExecutorSelectorProps) {
return (
<Combobox
options={options}
value={value}
onValueChange={onValueChange}
placeholder={placeholder}
emptyMessage="No executor found."
disabled={disabled}
dropdownLabel="Executor"
className={disabled ? undefined : CURSOR_POINTER_CLASS}
triggerClassName={triggerClassName}
showSearch={false}
/>
);
});
type ExecutorProfileSelectorProps = {
options: Array<{ value: string; label: string; renderLabel?: () => React.ReactNode }>;
value: string;
onValueChange: (value: string) => void;
disabled: boolean;
placeholder: string;
triggerClassName?: string;
};
export const ExecutorProfileSelector = memo(function ExecutorProfileSelector({
options,
value,
onValueChange,
disabled,
placeholder,
triggerClassName,
}: ExecutorProfileSelectorProps) {
return (
<Combobox
options={options}
value={value}
onValueChange={onValueChange}
placeholder={placeholder}
searchPlaceholder="Search profiles..."
emptyMessage="No profile found."
disabled={disabled}
dropdownLabel="Executor Profile"
className={disabled ? undefined : CURSOR_POINTER_CLASS}
triggerClassName={triggerClassName}
testId="executor-profile-selector"
/>
);
});
type InlineTaskNameProps = {
value: string;
onChange: (value: string) => void;
autoFocus?: boolean;
};
export const InlineTaskName = memo(function InlineTaskName({
value,
onChange,
autoFocus,
}: InlineTaskNameProps) {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (autoFocus && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [autoFocus]);
return (
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="task-name"
data-testid="task-title-input"
size={Math.max(value.length, 9)}
className="bg-transparent border-none outline-none focus:ring-0 text-sm font-medium min-w-0 rounded-md px-1.5 py-0.5 -mx-1.5 hover:bg-muted focus:bg-muted transition-colors"
/>
);
});
// Memoized description input to prevent re-rendering the entire dialog on every keystroke
type TaskFormInputsProps = {
isSessionMode: boolean;
autoFocus?: boolean;
initialDescription: string;
onDescriptionChange: (hasContent: boolean) => void;
onKeyDown: (e: React.KeyboardEvent) => void;
descriptionValueRef: React.RefObject<{ getValue: () => string } | null>;
disabled?: boolean;
placeholder?: string;
};
export const TaskFormInputs = memo(function TaskFormInputs({
isSessionMode,
autoFocus,
initialDescription,
onDescriptionChange,
onKeyDown,
descriptionValueRef,
disabled,
placeholder,
}: TaskFormInputsProps) {
const [description, setDescription] = useState(initialDescription);
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
const ref = descriptionValueRef as React.MutableRefObject<{ getValue: () => string } | null>;
if (ref) {
ref.current = { getValue: () => description };
}
}, [description, descriptionValueRef]);
// Auto-resize textarea + optional auto-focus with cursor at end
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}, [description]);
useEffect(() => {
if (!autoFocus) return;
const textarea = textareaRef.current;
if (!textarea) return;
textarea.focus();
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}, [autoFocus]);
const handleDescriptionChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
const hadContent = description.trim().length > 0;
const hasContent = newValue.trim().length > 0;
setDescription(newValue);
if (hadContent !== hasContent) {
onDescriptionChange(hasContent);
}
},
[description, onDescriptionChange],
);
return (
<div>
<Textarea
ref={textareaRef}
placeholder={
placeholder ??
(isSessionMode
? "Describe what you want the agent to do..."
: "Write a prompt for the agent...")
}
value={description}
onChange={handleDescriptionChange}
onKeyDown={onKeyDown}
data-testid="task-description-input"
rows={2}
className={
isSessionMode
? "min-h-[120px] max-h-[240px] resize-none overflow-auto text-[13px]"
: "min-h-[96px] max-h-[240px] resize-y overflow-auto text-[13px]"
}
required={isSessionMode}
disabled={disabled}
/>
</div>
);
});