import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { AuthenticationType, HttpMethod, RestAction, } from '../../types/project'; import { HTTP_METHODS, isAbsoluteHttpUrl, keyValueRowsToRecord, validateKeyValueRows, validateRestAction, } from './actionEditorUtils'; import type { KeyValueRow, RequestMapKeyKind, } from './actionEditorUtils'; import styles from './ActionInspector.module.css'; const AUTHENTICATION_OPTIONS: Array<{ value: AuthenticationType; label: string; }> = [ { value: 'anonymous', label: 'Anonymous' }, { value: 'bearerToken', label: 'Bearer token (Slice 3)' }, { value: 'basicAuth', label: 'Basic authentication (Slice 3)' }, { value: 'apiKeyHeader', label: 'API key header (Slice 3)' }, { value: 'apiKeyQueryParameter', label: 'API key query parameter (Slice 3)' }, ]; type KeyValueEditorProps = { fieldName: string; title: string; keyPlaceholder: string; valuePlaceholder?: string; keyKind?: RequestMapKeyKind; value: Record; onChange: (value: Record) => void; onValidityChange: (valid: boolean) => void; }; function makeRows(value: Record): KeyValueRow[] { return Object.entries(value).map(([key, entryValue], index) => ({ id: index + 1, key, value: entryValue, })); } function KeyValueEditor({ fieldName, title, keyPlaceholder, valuePlaceholder = 'Value or {{components.name.value}}', keyKind = 'generic', value, onChange, onValidityChange, }: KeyValueEditorProps): React.ReactElement { const [rows, setRows] = useState(() => makeRows(value)); const [error, setError] = useState(null); const nextRowId = useRef(rows.length + 1); const lastCanonicalValue = useRef(value); useEffect(() => { if (value === lastCanonicalValue.current) return; const nextRows = makeRows(value); lastCanonicalValue.current = value; nextRowId.current = nextRows.length + 1; setRows(nextRows); setError(null); onValidityChange(true); }, [onValidityChange, value]); const commitRows = useCallback( (nextRows: KeyValueRow[]) => { setRows(nextRows); const validationError = validateKeyValueRows(nextRows, keyKind); setError(validationError); onValidityChange(validationError === null); if (validationError) return; const nextRecord = keyValueRowsToRecord(nextRows); lastCanonicalValue.current = nextRecord; onChange(nextRecord); }, [keyKind, onChange, onValidityChange], ); const handleAdd = useCallback(() => { setRows((current) => [ ...current, { id: nextRowId.current++, key: '', value: '' }, ]); setError('Every row needs a key before it can be added to the project document.'); onValidityChange(false); }, [onValidityChange]); const handleChange = useCallback( (rowId: number, field: 'key' | 'value', nextValue: string) => { commitRows( rows.map((row) => row.id === rowId ? { ...row, [field]: nextValue } : row, ), ); }, [commitRows, rows], ); const handleRemove = useCallback( (rowId: number) => { commitRows(rows.filter((row) => row.id !== rowId)); }, [commitRows, rows], ); return (
{title}
{rows.length === 0 && (
No entries configured.
)} {rows.map((row) => (
handleChange(row.id, 'key', event.target.value)} /> handleChange(row.id, 'value', event.target.value)} />
))} {error && (
{error} Invalid rows remain local until corrected.
)}
); } export type RestActionEditorProps = { action: RestAction; actions: RestAction[]; onChange: (action: RestAction) => void; onDone: () => void; }; function RestActionEditor({ action, actions, onChange, onDone, }: RestActionEditorProps): React.ReactElement { const [nameDraft, setNameDraft] = useState(action.name); const [urlDraft, setUrlDraft] = useState(action.url); const [invalidMapFields, setInvalidMapFields] = useState>(() => new Set()); const lastEmittedAction = useRef(null); const nameDraftValid = nameDraft.trim().length > 0; const urlDraftValid = isAbsoluteHttpUrl(urlDraft); const hasInvalidDrafts = !nameDraftValid || !urlDraftValid || invalidMapFields.size > 0; const issues = useMemo( () => validateRestAction(action, actions), [action, actions], ); useEffect(() => { if (action === lastEmittedAction.current) { lastEmittedAction.current = null; return; } setNameDraft(action.name); setUrlDraft(action.url); setInvalidMapFields(new Set()); }, [action]); const update = useCallback( (field: K, value: RestAction[K]) => { const nextAction = { ...action, [field]: value }; lastEmittedAction.current = nextAction; onChange(nextAction); }, [action, onChange], ); const handleNameDraftChange = useCallback( (value: string) => { setNameDraft(value); if (value.trim()) update('name', value); }, [update], ); const handleUrlDraftChange = useCallback( (value: string) => { setUrlDraft(value); if (isAbsoluteHttpUrl(value)) update('url', value); }, [update], ); const handleMapValidityChange = useCallback( (fieldName: string, valid: boolean) => { setInvalidMapFields((current) => { const alreadyInvalid = current.has(fieldName); if ((valid && !alreadyInvalid) || (!valid && alreadyInvalid)) { return current; } const next = new Set(current); if (valid) next.delete(fieldName); else next.add(fieldName); return next; }); }, [], ); const editorId = `rest-action-editor-${action.id}`; return (
{ event.preventDefault(); if (hasInvalidDrafts) return; onDone(); }} >
Edit REST action
Valid edits update the canonical project document immediately.
{issues.length > 0 && (
{issues.map((issue, index) => (
{issue.message}
))}
)}