11import { monacoOnMountHandler } from "@/components/Monaco/utils" ;
2- import { Group , Radio } from "@mantine/core" ;
2+ import {
3+ Group ,
4+ Radio ,
5+ Table ,
6+ ActionIcon ,
7+ Tooltip ,
8+ Button ,
9+ FileButton ,
10+ Box ,
11+ TextInput ,
12+ } from "@mantine/core" ;
313import Editor from "@monaco-editor/react" ;
414import { useMemo } from "react" ;
5- import { BodyMode , RequestBody } from "../types/rest" ;
15+ import { BodyMode , RequestBody , KeyValue } from "../types/rest" ;
16+ import ParamsHeadersEditor from "./ParamsHeadersEditor" ;
17+ import { BsPlus , BsTrash3 } from "react-icons/bs" ;
618
719type Props = {
820 body : RequestBody ;
921 onChange : ( body : RequestBody ) => void ;
1022} ;
1123
12- const modes : BodyMode [ ] = [ "none" , "json" , "xml" , "text" ] ;
24+ const modes : BodyMode [ ] = [ "none" , "json" , "xml" , "text" , "multipart" ] ;
1325
1426export default function BodyEditor ( { body, onChange } : Props ) {
1527 const language = useMemo ( ( ) => {
@@ -20,20 +32,28 @@ export default function BodyEditor({ body, onChange }: Props) {
2032 } , [ body . mode ] ) ;
2133
2234 return (
23- < div style = { { height : 240 , display : "flex" , flexDirection : "column" , gap : 8 } } >
35+ < div style = { { display : "flex" , flexDirection : "column" , gap : 8 } } >
2436 < Group justify = "space-between" >
2537 < Radio . Group
2638 size = "xs"
2739 value = { body . mode }
2840 onChange = { v => {
29- onChange (
30- v === "none"
31- ? { mode : "none" }
32- : {
33- mode : v as Exclude < BodyMode , "none" | "form" | "multipart" > ,
34- text : ( body as any ) . text || "" ,
35- }
36- ) ;
41+ if ( v === "none" ) {
42+ onChange ( { mode : "none" } ) ;
43+ return ;
44+ }
45+ if ( v === "multipart" ) {
46+ onChange ( {
47+ mode : "multipart" ,
48+ fields : ( body as any ) . fields || [ ] ,
49+ files : ( body as any ) . files || [ ] ,
50+ } ) ;
51+ return ;
52+ }
53+ onChange ( {
54+ mode : v as Exclude < BodyMode , "none" | "multipart" > ,
55+ text : ( body as any ) . text || "" ,
56+ } ) ;
3757 } }
3858 name = "body-mode"
3959 >
@@ -50,23 +70,124 @@ export default function BodyEditor({ body, onChange }: Props) {
5070 style = { {
5171 flex : 1 ,
5272 minHeight : 180 ,
53- borderRadius : "var(--mantine-radius-md)" ,
54- overflow : "hidden" ,
5573 } }
5674 >
57- < Editor
58- height = "100%"
59- defaultLanguage = { language }
60- value = { ( body as any ) . text || "" }
61- onChange = { value => onChange ( { ...( body as any ) , text : value || "" } ) }
62- options = { {
63- minimap : { enabled : false } ,
64- fontSize : 13 ,
65- wordWrap : "on" ,
66- scrollBeyondLastLine : false ,
67- } }
68- onMount = { monacoOnMountHandler }
69- />
75+ { body . mode === "json" || body . mode === "xml" || body . mode === "text" ? (
76+ < Box
77+ h = "100%"
78+ w = "100%"
79+ style = { { borderRadius : "var(--mantine-radius-md)" , overflow : "hidden" } }
80+ >
81+ < Editor
82+ height = "100%"
83+ defaultLanguage = { language }
84+ value = { ( body as any ) . text || "" }
85+ onChange = { value => onChange ( { ...( body as any ) , text : value || "" } ) }
86+ options = { {
87+ minimap : { enabled : false } ,
88+ fontSize : 13 ,
89+ wordWrap : "on" ,
90+ scrollBeyondLastLine : false ,
91+ } }
92+ onMount = { monacoOnMountHandler }
93+ />
94+ </ Box >
95+ ) : null }
96+ { body . mode === "multipart" && (
97+ < div style = { { display : "flex" , flexDirection : "column" , gap : 8 } } >
98+ < ParamsHeadersEditor
99+ title = "Multipart Fields"
100+ rows = { ( body as any ) . fields || [ ] }
101+ onChange = { ( rows : KeyValue [ ] ) => onChange ( { ...( body as any ) , fields : rows } ) }
102+ />
103+ < Group justify = "space-between" style = { { marginBottom : 8 } } >
104+ < strong > Files</ strong >
105+ < Tooltip label = "Add file" >
106+ < ActionIcon
107+ variant = "default"
108+ onClick = { ( ) => {
109+ const files = ( body as any ) . files || [ ] ;
110+ const next = [
111+ ...files ,
112+ { id : crypto . randomUUID ( ) , field : "" , name : "" , file : undefined } ,
113+ ] ;
114+ onChange ( { ...( body as any ) , files : next } ) ;
115+ } }
116+ >
117+ < BsPlus size = { 18 } />
118+ </ ActionIcon >
119+ </ Tooltip >
120+ </ Group >
121+
122+ < Table striped withTableBorder withColumnBorders >
123+ < Table . Thead >
124+ < Table . Tr >
125+ < Table . Th > Field</ Table . Th >
126+ < Table . Th > File</ Table . Th >
127+ < Table . Th > Actions</ Table . Th >
128+ </ Table . Tr >
129+ </ Table . Thead >
130+ < Table . Tbody >
131+ { ( ( body as any ) . files || [ ] ) . map ( ( f : any ) => (
132+ < Table . Tr key = { f . id } >
133+ < Table . Td >
134+ < TextInput
135+ value = { f . field }
136+ placeholder = "field name"
137+ onChange = { e => {
138+ const files = ( body as any ) . files || [ ] ;
139+ onChange ( {
140+ ...( body as any ) ,
141+ files : files . map ( ( x : any ) =>
142+ x . id === f . id ? { ...x , field : e . currentTarget . value } : x
143+ ) ,
144+ } ) ;
145+ } }
146+ />
147+ </ Table . Td >
148+ < Table . Td >
149+ < Group >
150+ < div > { f . name || ( f . file && f . file . name ) || "(no file)" } </ div >
151+ < FileButton
152+ onChange = { file => {
153+ const files = ( body as any ) . files || [ ] ;
154+ onChange ( {
155+ ...( body as any ) ,
156+ files : files . map ( ( x : any ) =>
157+ x . id === f . id ? { ...x , file, name : file ?. name || x . name } : x
158+ ) ,
159+ } ) ;
160+ } }
161+ >
162+ { props => (
163+ < Button size = "xs" { ...props } >
164+ Choose
165+ </ Button >
166+ ) }
167+ </ FileButton >
168+ </ Group >
169+ </ Table . Td >
170+ < Table . Td style = { { width : 56 } } >
171+ < ActionIcon
172+ color = "red"
173+ variant = "subtle"
174+ onClick = { ( ) => {
175+ const files = ( body as any ) . files || [ ] ;
176+ onChange ( {
177+ ...( body as any ) ,
178+ files : files . filter ( ( x : any ) => x . id !== f . id ) ,
179+ } ) ;
180+ } }
181+ >
182+ < BsTrash3 size = { 16 } />
183+ </ ActionIcon >
184+ </ Table . Td >
185+ </ Table . Tr >
186+ ) ) }
187+ </ Table . Tbody >
188+ </ Table >
189+ </ div >
190+ ) }
70191 </ div >
71192 ) }
72193 </ div >
0 commit comments