Fix pair editor

This commit is contained in:
Gregory Schier
2025-11-02 05:52:36 -08:00
parent 0f9975339c
commit 2deb870bb6

View File

@@ -32,7 +32,6 @@ import { IconButton } from './IconButton';
import type { InputHandle, InputProps } from './Input'; import type { InputHandle, InputProps } from './Input';
import { Input } from './Input'; import { Input } from './Input';
import { ensurePairId } from './PairEditor.util'; import { ensurePairId } from './PairEditor.util';
import { PlainInput } from './PlainInput';
import type { RadioDropdownItem } from './RadioDropdown'; import type { RadioDropdownItem } from './RadioDropdown';
import { RadioDropdown } from './RadioDropdown'; import { RadioDropdown } from './RadioDropdown';
@@ -80,7 +79,7 @@ export type PairWithId = Pair & {
}; };
/** Max number of pairs to show before prompting the user to reveal the rest */ /** Max number of pairs to show before prompting the user to reveal the rest */
const MAX_INITIAL_PAIRS = 50; const MAX_INITIAL_PAIRS = 30;
export function PairEditor({ export function PairEditor({
allowFileValues, allowFileValues,
@@ -190,31 +189,21 @@ export function PairEditor({
[setPairsAndSave, pairs], [setPairsAndSave, pairs],
); );
const handleFocusName = useCallback((pair: Pair) => { const handleFocusName = useCallback(
setPairs((pairs) => { (pair: Pair) => {
const isLast = pair.id === pairs[pairs.length - 1]?.id; const isLast = pair.id === pairs[pairs.length - 1]?.id;
if (isLast) { if (isLast) setPairs([...pairs, emptyPair()]);
const prevPair = pairs[pairs.length - 1]; },
rowsRef.current[prevPair?.id ?? 'n/a']?.focusName(); [pairs],
return [...pairs, emptyPair()]; );
} else {
return pairs;
}
});
}, []);
const handleFocusValue = useCallback((pair: Pair) => { const handleFocusValue = useCallback(
setPairs((pairs) => { (pair: Pair) => {
const isLast = pair.id === pairs[pairs.length - 1]?.id; const isLast = pair.id === pairs[pairs.length - 1]?.id;
if (isLast) { if (isLast) setPairs([...pairs, emptyPair()]);
const prevPair = pairs[pairs.length - 1]; },
rowsRef.current[prevPair?.id ?? 'n/a']?.focusValue(); [pairs],
return [...pairs, emptyPair()]; );
} else {
return pairs;
}
});
}, []);
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } })); const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
@@ -282,73 +271,86 @@ export function PairEditor({
'-mr-2 pr-2', '-mr-2 pr-2',
// Pad to make room for the drag divider // Pad to make room for the drag divider
'pt-0.5', 'pt-0.5',
'grid grid-rows-[auto_1fr]',
)} )}
> >
<DndContext <div>
autoScroll <DndContext
sensors={sensors} autoScroll
onDragMove={onDragMove} sensors={sensors}
onDragEnd={onDragEnd} onDragMove={onDragMove}
onDragStart={onDragStart} onDragEnd={onDragEnd}
onDragCancel={onDragCancel} onDragStart={onDragStart}
collisionDetection={pointerWithin} onDragCancel={onDragCancel}
> collisionDetection={pointerWithin}
{pairs.map((p, i) => { >
if (!showAll && i > MAX_INITIAL_PAIRS) return null; {pairs.map((p, i) => {
if (!showAll && i > MAX_INITIAL_PAIRS) return null;
const isLast = i === pairs.length - 1; const isLast = i === pairs.length - 1;
return ( return (
<Fragment key={p.id}> <Fragment key={p.id}>
{hoveredIndex === i && <DropMarker />} {hoveredIndex === i && <DropMarker />}
<PairEditorRow <PairEditorRow
setRef={initPairEditorRow} setRef={initPairEditorRow}
allowFileValues={allowFileValues} allowFileValues={allowFileValues}
allowMultilineValues={allowMultilineValues} allowMultilineValues={allowMultilineValues}
className="py-1" className="py-1"
forcedEnvironmentId={forcedEnvironmentId} forcedEnvironmentId={forcedEnvironmentId}
forceUpdateKey={localForceUpdateKey} forceUpdateKey={localForceUpdateKey}
index={i} index={i}
isLast={isLast} isLast={isLast}
isDraggingGlobal={!!isDragging} isDraggingGlobal={!!isDragging}
nameAutocomplete={nameAutocomplete} nameAutocomplete={nameAutocomplete}
nameAutocompleteFunctions={nameAutocompleteFunctions} nameAutocompleteFunctions={nameAutocompleteFunctions}
nameAutocompleteVariables={nameAutocompleteVariables} nameAutocompleteVariables={nameAutocompleteVariables}
namePlaceholder={namePlaceholder} namePlaceholder={namePlaceholder}
nameValidate={nameValidate} nameValidate={nameValidate}
onChange={handleChange} onChange={handleChange}
onDelete={handleDelete} onDelete={handleDelete}
onFocusName={handleFocusName} onFocusName={handleFocusName}
onFocusValue={handleFocusValue} onFocusValue={handleFocusValue}
pair={p} pair={p}
stateKey={stateKey} stateKey={stateKey}
valueAutocomplete={valueAutocomplete} valueAutocomplete={valueAutocomplete}
valueAutocompleteFunctions={valueAutocompleteFunctions} valueAutocompleteFunctions={valueAutocompleteFunctions}
valueAutocompleteVariables={valueAutocompleteVariables} valueAutocompleteVariables={valueAutocompleteVariables}
valuePlaceholder={valuePlaceholder} valuePlaceholder={valuePlaceholder}
valueType={valueType} valueType={valueType}
valueValidate={valueValidate} valueValidate={valueValidate}
/> />
</Fragment> </Fragment>
); );
})} })}
{!showAll && pairs.length > MAX_INITIAL_PAIRS && ( {!showAll && pairs.length > MAX_INITIAL_PAIRS && (
<Button onClick={toggleShowAll} variant="border" className="m-2" size="xs"> <Button onClick={toggleShowAll} variant="border" className="m-2" size="xs">
Show {pairs.length - MAX_INITIAL_PAIRS} More Show {pairs.length - MAX_INITIAL_PAIRS} More
</Button> </Button>
)}
<DragOverlay dropAnimation={null}>
{isDragging && (
<PairEditorRow
namePlaceholder={namePlaceholder}
valuePlaceholder={valuePlaceholder}
className="opacity-80"
pair={isDragging}
index={0}
stateKey={null}
/>
)} )}
</DragOverlay> <DragOverlay dropAnimation={null}>
</DndContext> {isDragging && (
<PairEditorRow
namePlaceholder={namePlaceholder}
valuePlaceholder={valuePlaceholder}
className="opacity-80"
pair={isDragging}
index={0}
stateKey={null}
/>
)}
</DragOverlay>
</DndContext>
</div>
<div
// There's a weird bug where clicking below one of the above Codemirror inputs will cause
// it to focus. Putting this element here prevents that
aria-hidden
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
/>
</div> </div>
); );
} }
@@ -579,44 +581,29 @@ export function PairEditorRow({
'gap-0.5 grid-cols-1 grid-rows-2', 'gap-0.5 grid-cols-1 grid-rows-2',
)} )}
> >
{isLast ? ( <Input
// Use PlainInput for last ones because there's a unique bug where clicking below setRef={initNameInputRef}
// the Codemirror input focuses it. hideLabel
<PlainInput stateKey={`name.${pair.id}.${stateKey}`}
hideLabel disabled={disabled}
size="sm" wrapLines={false}
containerClassName={classNames(isLast && 'border-dashed')} readOnly={pair.readOnlyName || isDraggingGlobal}
className={classNames(isDraggingGlobal && 'pointer-events-none')} size="sm"
label="Name" required={!isLast && !!pair.enabled && !!pair.value}
name={`name[${index}]`} validate={nameValidate}
onFocus={handleFocusName} forcedEnvironmentId={forcedEnvironmentId}
placeholder={namePlaceholder ?? 'name'} forceUpdateKey={forceUpdateKey}
/> containerClassName={classNames('bg-surface', isLast && 'border-dashed')}
) : ( defaultValue={pair.name}
<Input label="Name"
setRef={initNameInputRef} name={`name[${index}]`}
hideLabel onChange={handleChangeName}
stateKey={`name.${pair.id}.${stateKey}`} onFocus={handleFocusName}
disabled={disabled} placeholder={namePlaceholder ?? 'name'}
wrapLines={false} autocomplete={nameAutocomplete}
readOnly={pair.readOnlyName || isDraggingGlobal} autocompleteVariables={nameAutocompleteVariables}
size="sm" autocompleteFunctions={nameAutocompleteFunctions}
required={!isLast && !!pair.enabled && !!pair.value} />
validate={nameValidate}
forcedEnvironmentId={forcedEnvironmentId}
forceUpdateKey={forceUpdateKey}
containerClassName={classNames('bg-surface', isLast && 'border-dashed')}
defaultValue={pair.name}
label="Name"
name={`name[${index}]`}
onChange={handleChangeName}
onFocus={handleFocusName}
placeholder={namePlaceholder ?? 'name'}
autocomplete={nameAutocomplete}
autocompleteVariables={nameAutocompleteVariables}
autocompleteFunctions={nameAutocompleteFunctions}
/>
)}
<div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center"> <div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center">
{pair.isFile ? ( {pair.isFile ? (
<SelectFile <SelectFile
@@ -626,20 +613,6 @@ export function PairEditorRow({
filePath={pair.value} filePath={pair.value}
onChange={handleChangeValueFile} onChange={handleChangeValueFile}
/> />
) : isLast ? (
// Use PlainInput for last ones because there's a unique bug where clicking below
// the Codemirror input focuses it.
<PlainInput
hideLabel
disabled={disabled}
size="sm"
containerClassName={classNames(isLast && 'border-dashed')}
label="Value"
name={`value[${index}]`}
className={classNames(isDraggingGlobal && 'pointer-events-none')}
onFocus={handleFocusValue}
placeholder={valuePlaceholder ?? 'value'}
/>
) : pair.value.includes('\n') ? ( ) : pair.value.includes('\n') ? (
<Button <Button
color="secondary" color="secondary"