diff --git a/src-web/components/HeaderEditor.tsx b/src-web/components/HeaderEditor.tsx index ebee50ac..ac4291a3 100644 --- a/src-web/components/HeaderEditor.tsx +++ b/src-web/components/HeaderEditor.tsx @@ -20,9 +20,12 @@ export function HeaderEditor({ request, className }: Props) { return { header: { name: '', value: '' }, id: Math.random().toString() }; }; - const [pairs, setPairs] = useState( - request.headers.map((h) => ({ header: h, id: Math.random().toString() })), - ); + const [pairs, setPairs] = useState(() => { + // Remove empty headers on initial render + const nonEmpty = request.headers.filter((h) => !(h.name === '' && h.value === '')); + const pairs = nonEmpty.map((h) => ({ header: h, id: Math.random().toString() })); + return [...pairs, newPair()]; + }); const setPairsAndSave = (fn: (pairs: PairWithId[]) => PairWithId[]) => { setPairs((oldPairs) => { @@ -41,15 +44,10 @@ export function HeaderEditor({ request, className }: Props) { ); }; + // Ensure there's always at least one pair useEffect(() => { - const lastPair = pairs[pairs.length - 1]; - if (lastPair === undefined) { - setPairsAndSave((pairs) => [...pairs, newPair()]); - return; - } - - if (lastPair.header.name !== '' || lastPair.header.value !== '') { - setPairsAndSave((pairs) => [...pairs, newPair()]); + if (pairs.length === 0) { + setPairs((pairs) => [...pairs, newPair()]); } }, [pairs]); @@ -68,6 +66,11 @@ export function HeaderEditor({ request, className }: Props) { pair={p} isLast={isLast} onChange={handleChangeHeader} + onFocus={() => { + if (isLast) { + setPairs((pairs) => [...pairs, newPair()]); + } + }} onDelete={isLast ? undefined : handleDelete} /> ); @@ -81,11 +84,13 @@ function FormRow({ pair, onChange, onDelete, + onFocus, isLast, }: { pair: PairWithId; onChange: (pair: PairWithId) => void; onDelete?: (pair: PairWithId) => void; + onFocus?: () => void; isLast?: boolean; }) { return ( @@ -97,7 +102,8 @@ function FormRow({ label="Name" name="name" onChange={(name) => onChange({ id: pair.id, header: { name } })} - placeholder="name" + onFocus={onFocus} + placeholder={isLast ? 'new name' : 'name'} useEditor={{ useTemplating: true }} /> onChange({ id: pair.id, header: { value } })} - placeholder="value" + onFocus={onFocus} + placeholder={isLast ? 'new value' : 'value'} useEditor={{ useTemplating: true }} /> {onDelete && ( diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 894f21d0..6ad81761 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -39,7 +39,7 @@ export function _Editor({ className, singleLine, }: _EditorProps) { - const cm = useRef<{ view: EditorView; langHolder: Compartment } | null>(null); + const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null); const wrapperRef = useRef(null); // Unmount the editor @@ -60,30 +60,38 @@ export function _Editor({ handleFocus.current = onFocus; }, [onFocus]); + // Update placeholder + const placeholderCompartment = useRef(new Compartment()); + useEffect(() => { + if (cm.current === null) return; + const effect = placeholderCompartment.current.reconfigure(placeholderExt(placeholder ?? '')); + cm.current?.view.dispatch({ effects: effect }); + }, [placeholder]); + // Update language extension when contentType changes useEffect(() => { if (cm.current === null) return; - const { view, langHolder } = cm.current; + const { view, languageCompartment } = cm.current; const ext = getLanguageExtension({ contentType, useTemplating }); - view.dispatch({ effects: langHolder.reconfigure(ext) }); + view.dispatch({ effects: languageCompartment.reconfigure(ext) }); }, [contentType]); // Initialize the editor when ref mounts useEffect(() => { if (wrapperRef.current === null || cm.current !== null) return; try { - const langHolder = new Compartment(); + const languageCompartment = new Compartment(); const langExt = getLanguageExtension({ contentType, useTemplating }); const state = EditorState.create({ doc: `${defaultValue ?? ''}`, extensions: [ - langHolder.of(langExt), + languageCompartment.of(langExt), + placeholderCompartment.current.of(placeholderExt(placeholder ?? '')), ...getExtensions({ container: wrapperRef.current, onChange: handleChange, onFocus: handleFocus, readOnly, - placeholder, singleLine, contentType, useTemplating, @@ -91,7 +99,7 @@ export function _Editor({ ], }); const view = new EditorView({ state, parent: wrapperRef.current }); - cm.current = { view, langHolder }; + cm.current = { view, languageCompartment }; if (autoFocus) view.focus(); } catch (e) { console.log('Failed to initialize Codemirror', e); @@ -122,15 +130,11 @@ function getExtensions({ container, readOnly, singleLine, - placeholder, onChange, onFocus, contentType, useTemplating, -}: Pick< - _EditorProps, - 'singleLine' | 'contentType' | 'useTemplating' | 'placeholder' | 'readOnly' -> & { +}: Pick<_EditorProps, 'singleLine' | 'contentType' | 'useTemplating' | 'readOnly'> & { container: HTMLDivElement | null; onChange: MutableRefObject<_EditorProps['onChange']>; onFocus: MutableRefObject<_EditorProps['onFocus']>; @@ -151,7 +155,6 @@ function getExtensions({ ...(!singleLine ? [multiLineExtensions] : []), ...(ext ? [ext] : []), ...(readOnly ? [EditorState.readOnly.of(true)] : []), - ...(placeholder ? [placeholderExt(placeholder)] : []), ...(singleLine ? [ EditorView.domEventHandlers({