Skip to content

Commit f89819c

Browse files
committed
feat(#186): support nested form request data
1 parent 68b6151 commit f89819c

File tree

4 files changed

+135
-90
lines changed

4 files changed

+135
-90
lines changed

resources/dist/_astro/App.54e7bd0f.js renamed to resources/dist/_astro/App.1fc4c015.js

Lines changed: 56 additions & 56 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/dist/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
<title>LRD - Laravel Request Docs</title>
88
<link rel="stylesheet" href="/request-docs/_astro/index.e4fa5488.css" /></head>
99
<body>
10-
<style>astro-island,astro-slot{display:contents}</style><script>(self.Astro=self.Astro||{}).only=t=>{(async()=>await(await t())())()},window.dispatchEvent(new Event("astro:only"));var l;{const c={0:t=>t,1:t=>JSON.parse(t,o),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,o)),5:t=>new Set(JSON.parse(t,o)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(JSON.parse(t)),9:t=>new Uint16Array(JSON.parse(t)),10:t=>new Uint32Array(JSON.parse(t))},o=(t,s)=>{if(t===""||!Array.isArray(s))return s;const[e,n]=s;return e in c?c[e](n):void 0};customElements.get("astro-island")||customElements.define("astro-island",(l=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(!this.hydrator||this.parentElement&&this.parentElement.closest("astro-island[ssr]"))return;const s=this.querySelectorAll("astro-slot"),e={},n=this.querySelectorAll("template[data-astro-template]");for(const r of n){const i=r.closest(this.tagName);!i||!i.isSameNode(this)||(e[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(const r of s){const i=r.closest(this.tagName);!i||!i.isSameNode(this)||(e[r.getAttribute("name")||"default"]=r.innerHTML)}const a=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),o):{};this.hydrator(this)(this.Component,a,e,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),window.removeEventListener("astro:hydrate",this.hydrate),window.dispatchEvent(new CustomEvent("astro:hydrate"))}}connectedCallback(){!this.hasAttribute("await-children")||this.firstChild?this.childrenConnectedCallback():new MutationObserver((s,e)=>{e.disconnect(),this.childrenConnectedCallback()}).observe(this,{childList:!0})}async childrenConnectedCallback(){window.addEventListener("astro:hydrate",this.hydrate);let s=this.getAttribute("before-hydration-url");s&&await import(s),this.start()}start(){const s=JSON.parse(this.getAttribute("opts")),e=this.getAttribute("client");if(Astro[e]===void 0){window.addEventListener(`astro:${e}`,()=>this.start(),{once:!0});return}Astro[e](async()=>{const n=this.getAttribute("renderer-url"),[a,{default:r}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),i=this.getAttribute("component-export")||"default";if(!i.includes("."))this.Component=a[i];else{this.Component=a;for(const d of i.split("."))this.Component=this.Component[d]}return this.hydrator=r,this.hydrate},s,this)}attributeChangedCallback(){this.hydrator&&this.hydrate()}},l.observedAttributes=["props"],l))}</script><astro-island uid="Z26kMUL" component-url="/request-docs/_astro/App.54e7bd0f.js" component-export="default" renderer-url="/request-docs/_astro/client.8c8eb78c.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;App&quot;,&quot;value&quot;:true}"></astro-island>
10+
<style>astro-island,astro-slot{display:contents}</style><script>(self.Astro=self.Astro||{}).only=t=>{(async()=>await(await t())())()},window.dispatchEvent(new Event("astro:only"));var l;{const c={0:t=>t,1:t=>JSON.parse(t,o),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(JSON.parse(t,o)),5:t=>new Set(JSON.parse(t,o)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(JSON.parse(t)),9:t=>new Uint16Array(JSON.parse(t)),10:t=>new Uint32Array(JSON.parse(t))},o=(t,s)=>{if(t===""||!Array.isArray(s))return s;const[e,n]=s;return e in c?c[e](n):void 0};customElements.get("astro-island")||customElements.define("astro-island",(l=class extends HTMLElement{constructor(){super(...arguments);this.hydrate=()=>{if(!this.hydrator||this.parentElement&&this.parentElement.closest("astro-island[ssr]"))return;const s=this.querySelectorAll("astro-slot"),e={},n=this.querySelectorAll("template[data-astro-template]");for(const r of n){const i=r.closest(this.tagName);!i||!i.isSameNode(this)||(e[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(const r of s){const i=r.closest(this.tagName);!i||!i.isSameNode(this)||(e[r.getAttribute("name")||"default"]=r.innerHTML)}const a=this.hasAttribute("props")?JSON.parse(this.getAttribute("props"),o):{};this.hydrator(this)(this.Component,a,e,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),window.removeEventListener("astro:hydrate",this.hydrate),window.dispatchEvent(new CustomEvent("astro:hydrate"))}}connectedCallback(){!this.hasAttribute("await-children")||this.firstChild?this.childrenConnectedCallback():new MutationObserver((s,e)=>{e.disconnect(),this.childrenConnectedCallback()}).observe(this,{childList:!0})}async childrenConnectedCallback(){window.addEventListener("astro:hydrate",this.hydrate);let s=this.getAttribute("before-hydration-url");s&&await import(s),this.start()}start(){const s=JSON.parse(this.getAttribute("opts")),e=this.getAttribute("client");if(Astro[e]===void 0){window.addEventListener(`astro:${e}`,()=>this.start(),{once:!0});return}Astro[e](async()=>{const n=this.getAttribute("renderer-url"),[a,{default:r}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),i=this.getAttribute("component-export")||"default";if(!i.includes("."))this.Component=a[i];else{this.Component=a;for(const d of i.split("."))this.Component=this.Component[d]}return this.hydrator=r,this.hydrate},s,this)}attributeChangedCallback(){this.hydrator&&this.hydrate()}},l.observedAttributes=["props"],l))}</script><astro-island uid="2tWEfI" component-url="/request-docs/_astro/App.1fc4c015.js" component-export="default" renderer-url="/request-docs/_astro/client.8c8eb78c.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;App&quot;,&quot;value&quot;:true}"></astro-island>
1111
</body></html>

ui/src/components/ApiAction.tsx

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import ApiActionInfo from './elements/ApiActionInfo'
1010
import ApiActionSQL from './elements/ApiActionSQL'
1111
import ApiActionLog from './elements/ApiActionLog'
1212
import ApiActionEvents from './elements/ApiActionEvents'
13+
import { objectToFormData } from '../libs/object';
1314

1415
interface Props {
1516
lrdDocsItem: IAPIInfo,
@@ -29,7 +30,7 @@ export default function ApiAction(props: Props) {
2930
const [sendingRequest, setSendingRequest] = useState(false);
3031
const [queryParams, setQueryParams] = useState('');
3132
const [bodyParams, setBodyParams] = useState('');
32-
const [fileParams, setFileParams] = useState(null);
33+
const [fileParams, setFileParams] = useState<FormData>();
3334
const [responseData, setResponseData] = useState("");
3435
const [sqlQueriesCount, setSqlQueriesCount] = useState(0);
3536
const [sqlData, setSqlData] = useState("");
@@ -43,15 +44,21 @@ export default function ApiAction(props: Props) {
4344
const [responseHeaders, setResponseHeaders] = useState("");
4445
const [activeTab, setActiveTab] = useState('info');
4546

46-
const handleFileChange = (files: any, file: any) => {
47-
const formData: any = new FormData()
48-
if (file.includes('.*')) {
49-
const fileParam = file.replace('.*', '')
47+
const handleFileChange = (files: any, key: any) => {
48+
const formData = fileParams || new FormData();
49+
const parts = key.split('.');
50+
const fileKey = key.split(".").reduce((current: string, part: string, index: number) => {
51+
if (index === parts.length - 1 && (part === '*' || !isNaN(Number(part)))) {
52+
return current
53+
}
54+
return !current ? part : `${current}[${part}]`
55+
}, '')
56+
if (key.includes('.*')) {
5057
for (let i = 0; i < files.length; i++) {
51-
formData.append(`${fileParam}[${i}]`, files[i]);
58+
formData.append(`${fileKey}[${i}]`, files[i]);
5259
}
5360
} else {
54-
formData.append(file, files[0])
61+
formData.append(fileKey, files[0])
5562
}
5663
setFileParams(formData)
5764
}
@@ -79,7 +86,7 @@ export default function ApiAction(props: Props) {
7986
}
8087
const headers = JSON.parse(requestHeaders)
8188
headers['X-Request-LRD'] = true
82-
if (fileParams != null) {
89+
if (fileParams) {
8390
delete headers['Content-Type']
8491
headers['Accept'] = 'multipart/form-data'
8592
}
@@ -90,13 +97,11 @@ export default function ApiAction(props: Props) {
9097
headers: headers,
9198
}
9299

100+
93101
if (method == 'POST' || method == 'PUT' || method == 'PATCH') {
94102
try {
95-
JSON.parse(bodyParams)
96103
if (fileParams != null) {
97-
for (const [key, value] of Object.entries(JSON.parse(bodyParams))) {
98-
fileParams.append(key, value)
99-
}
104+
objectToFormData(JSON.parse(bodyParams), fileParams as FormData)
100105
}
101106

102107
} catch (error: any) {
@@ -201,10 +206,14 @@ export default function ApiAction(props: Props) {
201206
let index = 0
202207
for (const [key] of Object.entries(lrdDocsItem.rules)) {
203208
index++
209+
const parts = key.split('.');
210+
const queryKey = parts.reduce((current: string, part: string) => {
211+
return current ? `${current}[${part !== '*' ? part : 0}]` : part
212+
}, '')
204213
if (index == 1) {
205-
queries += `?${key}=\n`
214+
queries += `?${queryKey}=\n`
206215
} else {
207-
queries += `&${key}=\n`
216+
queries += `&${queryKey}=\n`
208217
}
209218
}
210219
setQueryParams(queries)
@@ -217,30 +226,39 @@ export default function ApiAction(props: Props) {
217226
setCurlCommand(makeCurlCommand(host, lrdDocsItem.uri, method, cached, requestHeaders))
218227
return
219228
}
220-
const body: any = {}
221-
for (const [key, rule] of Object.entries(lrdDocsItem.rules)) {
229+
const body: any = Object.entries(lrdDocsItem.rules).reduce((acc, [key, rule]) => {
222230
if (rule.length == 0) {
223-
continue
231+
return acc
224232
}
225-
const theRule = rule[0].split("|")
226-
if (theRule.includes('file') || theRule.includes('image')) {
227-
continue
228-
}
229-
if (key.includes(".*")) {
230-
body[key] = []
231-
continue
233+
234+
const ruleObj = rule[0].split('|');
235+
236+
if (ruleObj.includes('file') || ruleObj.includes('image')) {
237+
return acc
232238
}
233-
if (key.includes(".")) {
234-
const keys = key.split(".")
235-
if (keys.length == 2) {
236-
body[keys[0]] = {}
237-
body[keys[0]][keys[1]] = ""
239+
240+
const keys = key.split('.');
241+
keys.reduce((current: any, key, index) => {
242+
key = key === "*" ? "0" : key;
243+
if (index === keys.length - 1) {
244+
if (!isNaN(Number(key))) {
245+
current = !Array.isArray(current) ? [] : current;
246+
return current
247+
}
248+
current[key] = ruleObj.includes('array') ? [] : "";
249+
} else {
250+
if (ruleObj.includes('array') || keys[index + 1] === "*" || !isNaN(Number(keys[index + 1]))) {
251+
current[key] = current[key] || [];
252+
} else {
253+
current[key] = current[key] || {};
254+
}
238255
}
239-
continue
240-
}
256+
return current[key];
257+
}, acc);
258+
259+
return acc;
260+
}, {})
241261

242-
body[key] = ""
243-
}
244262
const jsonBody = JSON.stringify(body, null, 2)
245263
setBodyParams(jsonBody)
246264
setCurlCommand(makeCurlCommand(host, lrdDocsItem.uri, method, jsonBody, requestHeaders))

ui/src/libs/object.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export const objectToFormData = (obj: any, formData?: FormData, namespace?: string): FormData => {
2+
formData = formData || new FormData();
3+
for (const property in obj) {
4+
if (obj[property] === undefined) {
5+
continue;
6+
}
7+
8+
const formKey = namespace ? `${namespace}[${property}]` : property;
9+
10+
if (obj[property] instanceof FileList) {
11+
for (let i = 0; i < obj[property].length; i++) {
12+
formData.append(`${formKey}[${i}]`, obj[property][i]);
13+
}
14+
} else if (obj[property] instanceof File) {
15+
formData.append(formKey, obj[property]);
16+
} else if (typeof obj[property] === 'object') {
17+
if (obj[property] instanceof Date) {
18+
formData.append(formKey, obj[property].toISOString());
19+
} else {
20+
objectToFormData(obj[property], formData, formKey);
21+
}
22+
} else {
23+
formData.append(formKey, obj[property].toString());
24+
}
25+
}
26+
return formData;
27+
}

0 commit comments

Comments
 (0)