diff --git a/package-lock.json b/package-lock.json index 776cbd5..5fabac0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", "@hookform/resolvers": "^3.3.2", + "@monaco-editor/react": "^4.6.0", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-label": "^2.0.2", @@ -4070,6 +4071,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -11593,6 +11618,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/monaco-editor": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.47.0.tgz", + "integrity": "sha512-VabVvHvQ9QmMwXu4du008ZDuyLnHs9j7ThVFsiJoXSOQk18+LF89N4ADzPbFenm0W4V2bGHnFBztIRQTgBfxzw==", + "peer": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -13143,6 +13174,11 @@ "node": ">=8" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", diff --git a/package.json b/package.json index e275123..36eb1f2 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", "@hookform/resolvers": "^3.3.2", + "@monaco-editor/react": "^4.6.0", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-label": "^2.0.2", diff --git a/src/entities/http-for-dummies.ts b/src/entities/http-for-dummies.ts index d3cfddf..da9334c 100644 --- a/src/entities/http-for-dummies.ts +++ b/src/entities/http-for-dummies.ts @@ -14,6 +14,10 @@ export function isRequestingToLocalhost(request: RequestSnapshot): boolean { ].some((localBeginning) => request.url.startsWith(localBeginning)); } +export function canMethodHaveBody(method: RequestSnapshot["method"]): boolean { + return method !== "GET" && method !== "HEAD"; +} + /** HTTP methods but as constants. */ export const HTTP_METHODS: ProjectRequestSpec["method"][] = [ "GET", diff --git a/src/entities/runtime-entities.ts b/src/entities/runtime-entities.ts index a0ec853..2ea849b 100644 --- a/src/entities/runtime-entities.ts +++ b/src/entities/runtime-entities.ts @@ -32,7 +32,7 @@ export interface RuntimeState { export interface RequestSnapshot { readonly url: string; readonly method: string; - readonly body: string; + readonly body: string | null; readonly headers: { key: string; value: string }[]; } diff --git a/src/features/project-workspace/BodyEditor.tsx b/src/features/project-workspace/BodyEditor.tsx new file mode 100644 index 0000000..5249435 --- /dev/null +++ b/src/features/project-workspace/BodyEditor.tsx @@ -0,0 +1,39 @@ +import { Editor, EditorProps } from "@monaco-editor/react"; +import { useCallback, useId } from "react"; + +export interface BodyEditorProps extends Omit { + name: string; +} + +export function BodyEditor(props: BodyEditorProps) { + const id = useId(); + + const { name, wrapperProps, ...restOfProps } = props; + const overridingWrapperProps = { ...wrapperProps, id }; + + const handleOnMount = useCallback(() => { + const wrapperEl = document.getElementById(id); + if (!wrapperEl) { + throw new Error("Unexpected missing wrapper during `BodyEditor` mount."); + } + + const [textareaEl] = wrapperEl.getElementsByTagName("textarea"); + if (!textareaEl) { + throw new Error( + "Unexpected missing `textarea` during `BodyEditor` mount." + ); + } + + textareaEl.setAttribute("name", name); + }, [id, name]); + + return ( + + ); +} diff --git a/src/features/project-workspace/Runtime.test.tsx b/src/features/project-workspace/Runtime.test.tsx index 0558a76..55ffd96 100644 --- a/src/features/project-workspace/Runtime.test.tsx +++ b/src/features/project-workspace/Runtime.test.tsx @@ -13,8 +13,25 @@ jest.mock("@/services/opfs-projects-shared-internals", () => ({ persistProject: jest.fn(), })); +jest.mock("./BodyEditor.tsx", () => ({ + BodyEditor: jest + .fn() + .mockImplementation(({ name, defaultValue, onChange }) => ( +
+