index.vue 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  1. <template>
  2. <div class="page">
  3. <pw-section class="yellow" label="Import" ref="import">
  4. <ul>
  5. <li>
  6. <button id="show-modal" @click="showModal = true">Import cURL</button>
  7. </li>
  8. </ul>
  9. <import-modal v-if="showModal" @close="showModal = false">
  10. <div slot="header">
  11. <ul>
  12. <li>
  13. <div class="flex-wrap">
  14. <h3 class="title">Import cURL</h3>
  15. <div>
  16. <button class="icon" @click="toggleModal">
  17. <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
  18. <path d="M23 20.168l-8.185-8.187 8.185-8.174-2.832-2.807-8.182 8.179-8.176-8.179-2.81 2.81 8.186 8.196-8.186 8.184 2.81 2.81 8.203-8.192 8.18 8.192z"/>
  19. </svg>
  20. </button>
  21. </div>
  22. </div>
  23. </li>
  24. </ul>
  25. </div>
  26. <div slot="body">
  27. <ul>
  28. <li>
  29. <textarea id="import-text" autofocus rows="8"></textarea>
  30. </li>
  31. </ul>
  32. </div>
  33. <div slot="footer">
  34. <ul>
  35. <li>
  36. <button @click="handleImport">Import</button>
  37. </li>
  38. </ul>
  39. </div>
  40. </import-modal>
  41. </pw-section>
  42. <pw-section class="blue" label="Request" ref="request">
  43. <ul>
  44. <li>
  45. <label for="method">Method</label>
  46. <select id="method" v-model="method">
  47. <option>GET</option>
  48. <option>HEAD</option>
  49. <option>POST</option>
  50. <option>PUT</option>
  51. <option>DELETE</option>
  52. <option>OPTIONS</option>
  53. <option>PATCH</option>
  54. </select>
  55. </li>
  56. <li>
  57. <label for="url">URL</label>
  58. <input :class="{ error: !isValidURL }" @keyup.enter="isValidURL ? sendRequest() : null" id="url" type="url" v-model="url">
  59. </li>
  60. <li>
  61. <label for="path">Path</label>
  62. <input @keyup.enter="isValidURL ? sendRequest() : null" id="path" v-model="path">
  63. </li>
  64. <div class="show-on-small-screen">
  65. <li>
  66. <label class="hide-on-small-screen" for="copyRequest">&nbsp;</label>
  67. <button class="icon" @click="copyRequest" id="copyRequest" ref="copyRequest" :disabled="!isValidURL">
  68. <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
  69. <path d="M22 6v16h-16v-16h16zm2-2h-20v20h20v-20zm-24 17v-21h21v2h-19v19h-2z" />
  70. </svg>
  71. <span>Share URL</span>
  72. </button>
  73. </li>
  74. <li>
  75. <label class="hide-on-small-screen" for="code">&nbsp;</label>
  76. <button class="icon" id="code" name="code" v-on:click="isHidden = !isHidden" :disabled="!isValidURL">
  77. <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" v-if="isHidden">
  78. <path d="M12.015 7c4.751 0 8.063 3.012 9.504 4.636-1.401 1.837-4.713 5.364-9.504 5.364-4.42 0-7.93-3.536-9.478-5.407 1.493-1.647 4.817-4.593 9.478-4.593zm0-2c-7.569 0-12.015 6.551-12.015 6.551s4.835 7.449 12.015 7.449c7.733 0 11.985-7.449 11.985-7.449s-4.291-6.551-11.985-6.551zm-.015 5c1.103 0 2 .897 2 2s-.897 2-2 2-2-.897-2-2 .897-2 2-2zm0-2c-2.209 0-4 1.792-4 4 0 2.209 1.791 4 4 4s4-1.791 4-4c0-2.208-1.791-4-4-4z" />
  79. </svg>
  80. <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" v-if="!isHidden">
  81. <path d="M19.604 2.562l-3.346 3.137c-1.27-.428-2.686-.699-4.243-.699-7.569 0-12.015 6.551-12.015 6.551s1.928 2.951 5.146 5.138l-2.911 2.909 1.414 1.414 17.37-17.035-1.415-1.415zm-6.016 5.779c-3.288-1.453-6.681 1.908-5.265 5.206l-1.726 1.707c-1.814-1.16-3.225-2.65-4.06-3.66 1.493-1.648 4.817-4.594 9.478-4.594.927 0 1.796.119 2.61.315l-1.037 1.026zm-2.883 7.431l5.09-4.993c1.017 3.111-2.003 6.067-5.09 4.993zm13.295-4.221s-4.252 7.449-11.985 7.449c-1.379 0-2.662-.291-3.851-.737l1.614-1.583c.715.193 1.458.32 2.237.32 4.791 0 8.104-3.527 9.504-5.364-.729-.822-1.956-1.99-3.587-2.952l1.489-1.46c2.982 1.9 4.579 4.327 4.579 4.327z" />
  82. </svg>
  83. <span>{{ isHidden ? 'Show Code' : 'Hide Code' }}</span>
  84. </button>
  85. </li>
  86. </div>
  87. <li>
  88. <label class="hide-on-small-screen" for="action">&nbsp;</label>
  89. <button :disabled="!isValidURL" @click="sendRequest" class="show" id="action" name="action" ref="sendButton">
  90. Send <span id="hidden-message">Again</span>
  91. <span>
  92. <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
  93. <path d="M5 3l3.057-3 11.943 12-11.943 12-3.057-3 9-9z"/>
  94. </svg>
  95. </span>
  96. </button>
  97. </li>
  98. </ul>
  99. </pw-section>
  100. <pw-section class="blue" label="Request Code" ref="requestCode" v-if="!isHidden">
  101. <ul>
  102. <li>
  103. <label for="requestType">Request Type</label>
  104. <select name="requestType" v-model="requestType">
  105. <option>JavaScript XHR</option>
  106. <option>Fetch</option>
  107. <option>cURL</option>
  108. </select>
  109. </li>
  110. </ul>
  111. <ul>
  112. <li>
  113. <div class="flex-wrap">
  114. <label for="generatedCode">Generated Code</label>
  115. <div>
  116. <button class="icon" @click="copyRequestCode" name="copyRequestCode" ref="copyRequestCode">
  117. <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
  118. <path d="M22 6v16h-16v-16h16zm2-2h-20v20h20v-20zm-24 17v-21h21v2h-19v19h-2z" />
  119. </svg>
  120. <span>Copy</span>
  121. </button>
  122. </div>
  123. </div>
  124. <textarea ref="generatedCode" name="generatedCode" rows="16" v-model="requestCode"></textarea>
  125. </li>
  126. </ul>
  127. </pw-section>
  128. <pw-section class="blue" label="Request Body" v-if="method === 'POST' || method === 'PUT' || method === 'PATCH'">
  129. <ul>
  130. <li>
  131. <autocomplete :source="validContentTypes" :spellcheck="false" v-model="contentType">Content Type
  132. </autocomplete>
  133. <span>
  134. <pw-toggle :on="rawInput" @change="rawInput = !rawInput">
  135. Raw input {{ rawInput ? "enabled" : "disabled" }}
  136. </pw-toggle>
  137. </span>
  138. </li>
  139. </ul>
  140. <div v-if="!rawInput">
  141. <ol v-for="(param, index) in bodyParams" :key="index">
  142. <li>
  143. <label :for="'bparam'+index">Key {{index + 1}}</label>
  144. <input :name="'bparam'+index" v-model="param.key" @keyup.prevent="setRouteQueryState" autofocus>
  145. </li>
  146. <li>
  147. <label :for="'bvalue'+index">Value {{index + 1}}</label>
  148. <input :name="'bvalue'+index" v-model="param.value" @keyup.prevent="setRouteQueryState">
  149. </li>
  150. <div>
  151. <li>
  152. <label class="hide-on-small-screen" for="request">&nbsp;</label>
  153. <button class="icon" @click="removeRequestBodyParam(index)" name="request">
  154. <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd">
  155. <path d="M5.662 23l-5.369-5.365c-.195-.195-.293-.45-.293-.707 0-.256.098-.512.293-.707l14.929-14.928c.195-.194.451-.293.707-.293.255 0 .512.099.707.293l7.071 7.073c.196.195.293.451.293.708 0 .256-.097.511-.293.707l-11.216 11.219h5.514v2h-12.343zm3.657-2l-5.486-5.486-1.419 1.414 4.076 4.072h2.829zm6.605-17.581l-10.677 10.68 5.658 5.659 10.676-10.682-5.657-5.657z" />
  156. </svg>
  157. </button>
  158. </li>
  159. </div>
  160. </ol>
  161. <ul>
  162. <li>
  163. <label for="addrequest">Action</label>
  164. <button @click="addRequestBodyParam" name="addrequest">Add New</button>
  165. </li>
  166. </ul>
  167. <ul>
  168. <li>
  169. <label for="request">Parameter List</label>
  170. <textarea name="request" readonly v-textarea-auto-height="rawRequestBody" v-model="rawRequestBody" placeholder="(add at least one parameter)" rows="1"></textarea>
  171. </li>
  172. </ul>
  173. </div>
  174. <div v-else>
  175. <textarea @keydown="formatRawParams" rows="16" v-model="rawParams" v-textarea-auto-height="rawParams"></textarea>
  176. </div>
  177. </pw-section>
  178. <pw-section class="purple" id="response" label="Response" ref="response">
  179. <ul>
  180. <li>
  181. <label for="status">status</label>
  182. <input :class="statusCategory ? statusCategory.className : ''" :value="response.status || '(waiting to send request)'" name="status" readonly type="text">
  183. </li>
  184. </ul>
  185. <ul v-for="(value, key) in response.headers" :key="key">
  186. <li>
  187. <label for="value">{{key}}</label>
  188. <input :value="value" name="value" readonly>
  189. </li>
  190. </ul>
  191. <ul v-if="response.body">
  192. <li>
  193. <div class="flex-wrap">
  194. <label for="body">response</label>
  195. <div>
  196. <button class="icon" @click="copyResponse" name="copyResponse" ref="copyResponse" v-if="response.body">
  197. <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
  198. <path d="M22 6v16h-16v-16h16zm2-2h-20v20h20v-20zm-24 17v-21h21v2h-19v19h-2z" />
  199. </svg>
  200. <span>Copy</span>
  201. </button>
  202. </div>
  203. </div>
  204. <div id="response-details-wrapper">
  205. <pre><code ref="responseBody" name="body" rows="16" placeholder="(waiting to send request)">{{response.body}}</code></pre>
  206. <iframe :class="{hidden: !previewEnabled}" class="covers-response" ref="previewFrame" src="about:blank"></iframe>
  207. </div>
  208. <div class="align-right" v-if="response.body && responseType === 'text/html'">
  209. <button class="icon" @click.prevent="togglePreview">
  210. <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" v-if="!previewEnabled">
  211. <path d="M12.015 7c4.751 0 8.063 3.012 9.504 4.636-1.401 1.837-4.713 5.364-9.504 5.364-4.42 0-7.93-3.536-9.478-5.407 1.493-1.647 4.817-4.593 9.478-4.593zm0-2c-7.569 0-12.015 6.551-12.015 6.551s4.835 7.449 12.015 7.449c7.733 0 11.985-7.449 11.985-7.449s-4.291-6.551-11.985-6.551zm-.015 5c1.103 0 2 .897 2 2s-.897 2-2 2-2-.897-2-2 .897-2 2-2zm0-2c-2.209 0-4 1.792-4 4 0 2.209 1.791 4 4 4s4-1.791 4-4c0-2.208-1.791-4-4-4z" />
  212. </svg>
  213. <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" v-if="previewEnabled">
  214. <path d="M19.604 2.562l-3.346 3.137c-1.27-.428-2.686-.699-4.243-.699-7.569 0-12.015 6.551-12.015 6.551s1.928 2.951 5.146 5.138l-2.911 2.909 1.414 1.414 17.37-17.035-1.415-1.415zm-6.016 5.779c-3.288-1.453-6.681 1.908-5.265 5.206l-1.726 1.707c-1.814-1.16-3.225-2.65-4.06-3.66 1.493-1.648 4.817-4.594 9.478-4.594.927 0 1.796.119 2.61.315l-1.037 1.026zm-2.883 7.431l5.09-4.993c1.017 3.111-2.003 6.067-5.09 4.993zm13.295-4.221s-4.252 7.449-11.985 7.449c-1.379 0-2.662-.291-3.851-.737l1.614-1.583c.715.193 1.458.32 2.237.32 4.791 0 8.104-3.527 9.504-5.364-.729-.822-1.956-1.99-3.587-2.952l1.489-1.46c2.982 1.9 4.579 4.327 4.579 4.327z" />
  215. </svg>
  216. <span>{{ previewEnabled ? 'Hide Preview' : 'Preview HTML' }}</span>
  217. </button>
  218. </div>
  219. </li>
  220. </ul>
  221. </pw-section>
  222. <pw-section class="green" collapsed label="Authentication">
  223. <ul>
  224. <li>
  225. <label for="auth">Authentication Type</label>
  226. <select v-model="auth">
  227. <option>None</option>
  228. <option>Basic</option>
  229. <option>Bearer Token</option>
  230. </select>
  231. </li>
  232. </ul>
  233. <ul v-if="auth === 'Basic'">
  234. <li>
  235. <label for="http_basic_user">User</label>
  236. <input v-model="httpUser">
  237. </li>
  238. <li>
  239. <label for="http_basic_passwd">Password</label>
  240. <input type="password" v-model="httpPassword">
  241. </li>
  242. </ul>
  243. <ul v-if="auth === 'Bearer Token'">
  244. <li>
  245. <label for="bearer_token">Token</label>
  246. <input v-model="bearerToken">
  247. </li>
  248. </ul>
  249. </pw-section>
  250. <pw-section class="orange" collapsed label="Headers">
  251. <ol v-for="(header, index) in headers" :key="index">
  252. <li>
  253. <label :for="'header'+index">Header {{index + 1}}</label>
  254. <input :name="'header'+index" v-model="header.key" @keyup.prevent="setRouteQueryState" autofocus>
  255. </li>
  256. <li>
  257. <label :for="'value'+index">Value {{index + 1}}</label>
  258. <input :name="'value'+index" v-model="header.value" @keyup.prevent="setRouteQueryState">
  259. </li>
  260. <div>
  261. <li>
  262. <label class="hide-on-small-screen" for="header">&nbsp;</label>
  263. <button class="icon" @click="removeRequestHeader(index)" name="header">
  264. <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd">
  265. <path d="M5.662 23l-5.369-5.365c-.195-.195-.293-.45-.293-.707 0-.256.098-.512.293-.707l14.929-14.928c.195-.194.451-.293.707-.293.255 0 .512.099.707.293l7.071 7.073c.196.195.293.451.293.708 0 .256-.097.511-.293.707l-11.216 11.219h5.514v2h-12.343zm3.657-2l-5.486-5.486-1.419 1.414 4.076 4.072h2.829zm6.605-17.581l-10.677 10.68 5.658 5.659 10.676-10.682-5.657-5.657z" />
  266. </svg>
  267. </button>
  268. </li>
  269. </div>
  270. </ol>
  271. <ul>
  272. <li>
  273. <button @click="addRequestHeader" name="add">Add New</button>
  274. </li>
  275. </ul>
  276. <ul>
  277. <li>
  278. <label for="request">Header List</label>
  279. <textarea name="request" readonly v-textarea-auto-height="headerString" v-model="headerString" placeholder="(add at least one header)" rows="1"></textarea>
  280. </li>
  281. </ul>
  282. </pw-section>
  283. <pw-section class="pink" collapsed label="Parameters">
  284. <ol v-for="(param, index) in params" :key="index">
  285. <li>
  286. <label :for="'param'+index">Parameter {{index + 1}}</label>
  287. <input :name="'param'+index" v-model="param.key" autofocus>
  288. </li>
  289. <li>
  290. <label :for="'value'+index">Value {{index + 1}}</label>
  291. <input :name="'value'+index" v-model="param.value">
  292. </li>
  293. <div>
  294. <li>
  295. <label class="hide-on-small-screen" for="param">&nbsp;</label>
  296. <button class="icon" @click="removeRequestParam(index)" name="param">
  297. <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd">
  298. <path d="M5.662 23l-5.369-5.365c-.195-.195-.293-.45-.293-.707 0-.256.098-.512.293-.707l14.929-14.928c.195-.194.451-.293.707-.293.255 0 .512.099.707.293l7.071 7.073c.196.195.293.451.293.708 0 .256-.097.511-.293.707l-11.216 11.219h5.514v2h-12.343zm3.657-2l-5.486-5.486-1.419 1.414 4.076 4.072h2.829zm6.605-17.581l-10.677 10.68 5.658 5.659 10.676-10.682-5.657-5.657z" />
  299. </svg>
  300. </button>
  301. </li>
  302. </div>
  303. </ol>
  304. <ul>
  305. <li>
  306. <button @click="addRequestParam" name="add">Add New</button>
  307. </li>
  308. </ul>
  309. <ul>
  310. <li>
  311. <label for="request">Parameter List</label>
  312. <textarea name="request" readonly v-textarea-auto-height="queryString" v-model="queryString" placeholder="(add at least one parameter)" rows="1"></textarea>
  313. </li>
  314. </ul>
  315. </pw-section>
  316. <history @useHistory="handleUseHistory" ref="historyComponent" />
  317. </div>
  318. </template>
  319. <script>
  320. import autocomplete from '../components/autocomplete';
  321. import history from "../components/history";
  322. import section from "../components/section";
  323. import textareaAutoHeight from "../directives/textareaAutoHeight";
  324. import toggle from "../components/toggle";
  325. import import_modal from "../components/modal";
  326. import parseCurlCommand from '../assets/js/curlparser.js';
  327. import hljs from 'highlight.js';
  328. import 'highlight.js/styles/dracula.css';
  329. const statusCategories = [{
  330. name: 'informational',
  331. statusCodeRegex: new RegExp(/[1][0-9]+/),
  332. className: 'info-response'
  333. },
  334. {
  335. name: 'successful',
  336. statusCodeRegex: new RegExp(/[2][0-9]+/),
  337. className: 'success-response'
  338. },
  339. {
  340. name: 'redirection',
  341. statusCodeRegex: new RegExp(/[3][0-9]+/),
  342. className: 'redir-response'
  343. },
  344. {
  345. name: 'client error',
  346. statusCodeRegex: new RegExp(/[4][0-9]+/),
  347. className: 'cl-error-response'
  348. },
  349. {
  350. name: 'server error',
  351. statusCodeRegex: new RegExp(/[5][0-9]+/),
  352. className: 'sv-error-response'
  353. },
  354. {
  355. // this object is a catch-all for when no other objects match and should always be last
  356. name: 'unknown',
  357. statusCodeRegex: new RegExp(/.*/),
  358. className: 'missing-data-response'
  359. }
  360. ];
  361. const parseHeaders = xhr => {
  362. const headers = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
  363. const headerMap = {};
  364. headers.forEach(line => {
  365. const parts = line.split(': ');
  366. const header = parts.shift().toLowerCase();
  367. const value = parts.join(': ');
  368. headerMap[header] = value
  369. });
  370. return headerMap
  371. };
  372. export const findStatusGroup = responseStatus => statusCategories.find(status => status.statusCodeRegex.test(responseStatus));
  373. export default {
  374. directives: {
  375. textareaAutoHeight
  376. },
  377. components: {
  378. 'pw-section': section,
  379. 'pw-toggle': toggle,
  380. 'import-modal': import_modal,
  381. history,
  382. autocomplete,
  383. },
  384. data() {
  385. return {
  386. showModal: false,
  387. copyButton: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path d="M22 6v16h-16v-16h16zm2-2h-20v20h20v-20zm-24 17v-21h21v2h-19v19h-2z" /></svg>',
  388. copiedButton: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path d="M22 2v20h-20v-20h20zm2-2h-24v24h24v-24zm-5.541 8.409l-1.422-1.409-7.021 7.183-3.08-2.937-1.395 1.435 4.5 4.319 8.418-8.591z"/></svg>',
  389. method: 'GET',
  390. url: 'https://reqres.in',
  391. auth: 'None',
  392. path: '/api/users',
  393. httpUser: '',
  394. httpPassword: '',
  395. bearerToken: '',
  396. headers: [],
  397. params: [],
  398. bodyParams: [],
  399. rawParams: '',
  400. rawInput: false,
  401. contentType: 'application/json',
  402. requestType: 'JavaScript XHR',
  403. isHidden: true,
  404. response: {
  405. status: '',
  406. headers: '',
  407. body: ''
  408. },
  409. previewEnabled: false,
  410. /**
  411. * These are content types that can be automatically
  412. * serialized by postwoman.
  413. */
  414. knownContentTypes: [
  415. 'application/json',
  416. 'application/x-www-form-urlencoded'
  417. ],
  418. /**
  419. * These are a list of Content Types known to Postwoman.
  420. */
  421. validContentTypes: [
  422. 'application/json',
  423. 'application/hal+json',
  424. 'application/xml',
  425. 'application/x-www-form-urlencoded',
  426. 'text/html',
  427. 'text/plain'
  428. ]
  429. }
  430. },
  431. watch: {
  432. contentType(val) {
  433. this.rawInput = !this.knownContentTypes.includes(val);
  434. },
  435. rawInput (status) {
  436. if (status && this.rawParams === '') this.rawParams = '{}'
  437. else this.setRouteQueryState()
  438. },
  439. 'response.body': function (val) {
  440. var responseText = document.querySelector("div#response-details-wrapper pre code") != null ? document.querySelector("div#response-details-wrapper pre code") : null;
  441. if (responseText) {
  442. if (document.querySelector('.hljs') !== null && responseText.innerHTML.indexOf('<span class="hljs') !== -1) {
  443. responseText.removeAttribute("class");
  444. responseText.innerHTML = null;
  445. responseText.innerText = this.response.body;
  446. } else if (responseText && this.response.body != "(waiting to send request)" && this.response.body != "Loading..." && this.response.body != "See JavaScript console (F12) for details.") {
  447. responseText.innerText = this.responseType == 'application/json' ? JSON.stringify(this.response.body, null, 2) : this.response.body;
  448. hljs.highlightBlock(document.querySelector("div#response-details-wrapper pre code"));
  449. } else {
  450. responseText.innerText = this.response.body
  451. }
  452. }
  453. }
  454. },
  455. computed: {
  456. statusCategory() {
  457. return findStatusGroup(this.response.status);
  458. },
  459. isValidURL() {
  460. const protocol = '^(https?:\\/\\/)?';
  461. const validIP = new RegExp(protocol + "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$");
  462. const validHostname = new RegExp(protocol + "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$");
  463. return validIP.test(this.url) || validHostname.test(this.url);
  464. },
  465. hasRequestBody() {
  466. return ['POST', 'PUT', 'PATCH'].includes(this.method);
  467. },
  468. rawRequestBody() {
  469. const {
  470. bodyParams
  471. } = this
  472. if (this.contentType === 'application/json') {
  473. try {
  474. const obj = JSON.parse(`{${bodyParams.filter(({key}) => !!key).map(({key, value}) => `
  475. "${key}": "${value}"
  476. `).join()}}`)
  477. return JSON.stringify(obj)
  478. } catch (ex) {
  479. return 'invalid'
  480. }
  481. } else {
  482. return bodyParams
  483. .filter(({
  484. key
  485. }) => !!key)
  486. .map(({
  487. key,
  488. value
  489. }) => `${key}=${encodeURIComponent(value)}`).join('&')
  490. }
  491. },
  492. headerString() {
  493. const result = this.headers
  494. .filter(({
  495. key
  496. }) => !!key)
  497. .map(({
  498. key,
  499. value
  500. }) => `${key}: ${value}`).join(',\n')
  501. return result == '' ? '' : `${result}`
  502. },
  503. queryString() {
  504. const result = this.params
  505. .filter(({
  506. key
  507. }) => !!key)
  508. .map(({
  509. key,
  510. value
  511. }) => `${key}=${encodeURIComponent(value)}`).join('&')
  512. return result === '' ? '' : `?${result}`
  513. },
  514. responseType() {
  515. return (this.response.headers['content-type'] || '').split(';')[0].toLowerCase();
  516. },
  517. requestCode() {
  518. if (this.requestType == 'JavaScript XHR') {
  519. var requestString = []
  520. requestString.push('const xhr = new XMLHttpRequest()');
  521. const user = this.auth === 'Basic' ? this.httpUser : null
  522. const pswd = this.auth === 'Basic' ? this.httpPassword : null
  523. requestString.push('xhr.open(' + this.method + ', ' + this.url + this.path + this.queryString + ', true, ' + user + ', ' + pswd + ')');
  524. if (this.auth === 'Bearer Token') {
  525. requestString.push("xhr.setRequestHeader('Authorization', 'Bearer ' + " + this.bearerToken + ")");
  526. }
  527. if (this.headers) {
  528. this.headers.forEach(function(element) {
  529. requestString.push('xhr.setRequestHeader(' + element.key + ', ' + element.value + ')');
  530. })
  531. }
  532. if (this.method === 'POST' || this.method === 'PUT') {
  533. const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody;
  534. requestString.push("xhr.setRequestHeader('Content-Length', " + requestBody.length + ")")
  535. requestString.push("xhr.setRequestHeader('Content-Type', `" + this.contentType + "; charset=utf-8`)")
  536. requestString.push("xhr.send(" + requestBody + ")")
  537. } else {
  538. requestString.push('xhr.send()')
  539. }
  540. return requestString.join('\n');
  541. } else if (this.requestType == 'Fetch') {
  542. var requestString = [];
  543. var headers = [];
  544. requestString.push('fetch(' + this.url + this.path + this.queryString + ', {\n')
  545. requestString.push(' method: "' + this.method + '",\n')
  546. if (this.auth === 'Basic') {
  547. var basic = this.httpUser + ':' + this.httpPassword;
  548. headers.push(' "Authorization": "Basic ' + window.btoa(unescape(encodeURIComponent(basic))) + ',\n')
  549. } else if (this.auth === 'Bearer Token') {
  550. headers.push(' "Authorization": "Bearer Token ' + this.bearerToken + ',\n')
  551. }
  552. if (this.method === 'POST' || this.method === 'PUT') {
  553. const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody;
  554. requestString.push(' body: ' + requestBody + ',\n')
  555. headers.push(' "Content-Length": ' + requestBody.length + ',\n')
  556. headers.push(' "Content-Type": "' + this.contentType + '; charset=utf-8",\n')
  557. }
  558. if (this.headers) {
  559. this.headers.forEach(function(element) {
  560. headers.push(' "' + element.key + '": "' + element.value + '",\n');
  561. })
  562. }
  563. headers = headers.join('').slice(0, -3);
  564. requestString.push(' headers: {\n' + headers + '\n },\n')
  565. requestString.push(' credentials: "same-origin"\n')
  566. requestString.push(')}).then(function(response) {\n')
  567. requestString.push(' response.status\n')
  568. requestString.push(' response.statusText\n')
  569. requestString.push(' response.headers\n')
  570. requestString.push(' response.url\n\n')
  571. requestString.push(' return response.text()\n')
  572. requestString.push(')}, function(error) {\n')
  573. requestString.push(' error.message\n')
  574. requestString.push(')}')
  575. return requestString.join('');
  576. } else if (this.requestType == 'cURL') {
  577. var requestString = [];
  578. requestString.push('curl -X ' + this.method + ' \\\n')
  579. requestString.push(" '" + this.url + this.path + this.queryString + "' \\\n")
  580. if (this.auth === 'Basic') {
  581. var basic = this.httpUser + ':' + this.httpPassword;
  582. requestString.push(" -H 'Authorization: Basic " + window.btoa(unescape(encodeURIComponent(basic))) + "' \\\n")
  583. } else if (this.auth === 'Bearer Token') {
  584. requestString.push(" -H 'Authorization: Bearer Token " + this.bearerToken + "' \\\n")
  585. }
  586. if (this.headers) {
  587. this.headers.forEach(function(element) {
  588. requestString.push(" -H '" + element.key + ": " + element.value + "' \\\n");
  589. })
  590. }
  591. if (this.method === 'POST' || this.method === 'PUT') {
  592. const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody;
  593. requestString.push(" -H 'Content-Length: " + requestBody.length + "' \\\n")
  594. requestString.push(" -H 'Content-Type: " + this.contentType + "; charset=utf-8' \\\n")
  595. requestString.push(" -d '" + requestBody + "' \\\n")
  596. }
  597. return requestString.join('').slice(0, -4);
  598. }
  599. }
  600. },
  601. methods: {
  602. handleUseHistory({
  603. method,
  604. url,
  605. path
  606. }) {
  607. this.method = method;
  608. this.url = url;
  609. this.path = path;
  610. this.$refs.request.$el.scrollIntoView({
  611. behavior: 'smooth'
  612. });
  613. },
  614. async sendRequest() {
  615. if (!this.isValidURL) {
  616. alert('Please check the formatting of the URL.');
  617. return;
  618. }
  619. // Start showing the loading bar as soon as possible.
  620. // The nuxt axios module will hide it when the request is made.
  621. this.$nuxt.$loading.start();
  622. if (this.$refs.response.$el.classList.contains('hidden')) {
  623. this.$refs.response.$el.classList.toggle('hidden')
  624. }
  625. this.$refs.response.$el.scrollIntoView({
  626. behavior: 'smooth'
  627. });
  628. this.previewEnabled = false;
  629. this.response.status = 'Fetching...';
  630. this.response.body = 'Loading...';
  631. const auth = this.auth === 'Basic' ? {
  632. username: this.httpUser,
  633. password: this.httpPassword
  634. } : null;
  635. let headers = {};
  636. // If the request has a request body, we want to ensure Content-Length and
  637. // Content-Type are sent.
  638. let requestBody;
  639. if (this.hasRequestBody) {
  640. requestBody = this.rawInput ? this.rawParams : this.rawRequestBody;
  641. Object.assign(headers, {
  642. //'Content-Length': requestBody.length,
  643. 'Content-Type': `${this.contentType}; charset=utf-8`
  644. });
  645. }
  646. // If the request uses a token for auth, we want to make sure it's sent here.
  647. if (this.auth === 'Bearer Token') headers['Authorization'] = `Bearer ${this.bearerToken}`;
  648. headers = Object.assign(
  649. // Clone the app headers object first, we don't want to
  650. // mutate it with the request headers added by default.
  651. Object.assign({}, this.headers),
  652. // We make our temporary headers object the source so
  653. // that you can override the added headers if you
  654. // specify them.
  655. headers
  656. );
  657. try {
  658. const payload = await this.$axios({
  659. method: this.method,
  660. url: this.url + this.path + this.queryString,
  661. auth,
  662. headers,
  663. data: requestBody ? requestBody.toString() : null
  664. });
  665. (() => {
  666. const status = this.response.status = payload.status;
  667. const headers = this.response.headers = payload.headers;
  668. // We don't need to bother parsing JSON, axios already handles it for us!
  669. const body = this.response.body = payload.data;
  670. const date = new Date().toLocaleDateString();
  671. const time = new Date().toLocaleTimeString();
  672. // Addition of an entry to the history component.
  673. const entry = {
  674. status,
  675. date,
  676. time,
  677. method: this.method,
  678. url: this.url,
  679. path: this.path
  680. };
  681. this.$refs.historyComponent.addEntry(entry);
  682. })();
  683. } catch (error) {
  684. if (error.response) {
  685. this.response.headers = error.response.headers;
  686. this.response.status = error.response.status;
  687. this.response.body = error.response.data;
  688. // Addition of an entry to the history component.
  689. const entry = {
  690. status: this.response.status,
  691. date: new Date().toLocaleDateString(),
  692. time: new Date().toLocaleTimeString(),
  693. method: this.method,
  694. url: this.url,
  695. path: this.path
  696. };
  697. this.$refs.historyComponent.addEntry(entry);
  698. return;
  699. }
  700. this.response.status = error.message;
  701. this.response.body = "See JavaScript console (F12) for details.";
  702. }
  703. },
  704. addRequestHeader() {
  705. this.headers.push({
  706. key: '',
  707. value: ''
  708. });
  709. return false
  710. },
  711. removeRequestHeader(index) {
  712. this.headers.splice(index, 1)
  713. },
  714. addRequestParam() {
  715. this.params.push({
  716. key: '',
  717. value: ''
  718. })
  719. return false
  720. },
  721. removeRequestParam(index) {
  722. this.params.splice(index, 1)
  723. },
  724. addRequestBodyParam() {
  725. this.bodyParams.push({
  726. key: '',
  727. value: ''
  728. })
  729. return false
  730. },
  731. removeRequestBodyParam(index) {
  732. this.bodyParams.splice(index, 1)
  733. },
  734. formatRawParams(event) {
  735. if ((event.which !== 13 && event.which !== 9)) {
  736. return;
  737. }
  738. const textBody = event.target.value;
  739. const textBeforeCursor = textBody.substring(0, event.target.selectionStart);
  740. const textAfterCursor = textBody.substring(event.target.selectionEnd);
  741. if (event.which === 13) {
  742. event.preventDefault();
  743. const oldSelectionStart = event.target.selectionStart;
  744. const lastLine = textBeforeCursor.split('\n').slice(-1)[0];
  745. const rightPadding = lastLine.match(/([\s\t]*).*/)[1] || "";
  746. event.target.value = textBeforeCursor + '\n' + rightPadding + textAfterCursor;
  747. setTimeout(() => event.target.selectionStart = event.target.selectionEnd = oldSelectionStart + rightPadding.length + 1, 1);
  748. } else if (event.which === 9) {
  749. event.preventDefault();
  750. const oldSelectionStart = event.target.selectionStart;
  751. event.target.value = textBeforeCursor + '\xa0\xa0' + textAfterCursor;
  752. event.target.selectionStart = event.target.selectionEnd = oldSelectionStart + 2;
  753. return false;
  754. }
  755. },
  756. copyRequest() {
  757. if (navigator.share) {
  758. let time = new Date().toLocaleTimeString();
  759. let date = new Date().toLocaleDateString();
  760. navigator.share({
  761. text: `Postwoman • API request builder at ${time} on ${date}`,
  762. url: window.location.href
  763. }).then(() => {
  764. // console.log('Thanks for sharing!');
  765. })
  766. .catch(console.error);
  767. } else {
  768. this.$refs.copyRequest.innerHTML = this.copiedButton + '<span>Copied</span>';
  769. var dummy = document.createElement('input');
  770. document.body.appendChild(dummy);
  771. dummy.value = window.location.href;
  772. dummy.select();
  773. document.execCommand('copy');
  774. document.body.removeChild(dummy);
  775. setTimeout(() => this.$refs.copyRequest.innerHTML = this.copyButton + '<span>Share URL</span>', 1500)
  776. }
  777. },
  778. copyRequestCode() {
  779. this.$refs.copyRequestCode.innerHTML = this.copiedButton + '<span>Copied</span>';
  780. this.$refs.generatedCode.select();
  781. document.execCommand("copy");
  782. setTimeout(() => this.$refs.copyRequestCode.innerHTML = this.copyButton + '<span>Copy</span>', 1500)
  783. },
  784. copyResponse() {
  785. this.$refs.copyResponse.innerHTML = this.copiedButton + '<span>Copied</span>';
  786. // Creates a textarea element
  787. var aux = document.createElement("textarea");
  788. var copy = this.responseType == 'application/json' ? JSON.stringify(this.response.body) : this.response.body;
  789. // Adds response body to the new textarea
  790. aux.innerText = copy;
  791. // Append the textarea to the body
  792. document.body.appendChild(aux);
  793. // Highlight the content
  794. aux.select();
  795. document.execCommand('copy');
  796. // Remove the input from the body
  797. document.body.removeChild(aux);
  798. setTimeout(() => this.$refs.copyResponse.innerHTML = this.copyButton + '<span>Copy</span>', 1500)
  799. },
  800. togglePreview() {
  801. this.previewEnabled = !this.previewEnabled;
  802. if (this.previewEnabled) {
  803. // If you want to add 'preview' support for other response types,
  804. // just add them here.
  805. if (this.responseType === "text/html") {
  806. // If the preview already has that URL loaded, let's not bother re-loading it all.
  807. if (this.$refs.previewFrame.getAttribute('data-previewing-url') === this.url)
  808. return;
  809. // Use DOMParser to parse document HTML.
  810. const previewDocument = new DOMParser().parseFromString(this.response.body, this.responseType);
  811. // Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
  812. previewDocument.head.innerHTML = `<base href="${this.url}">` + previewDocument.head.innerHTML;
  813. // Finally, set the iframe source to the resulting HTML.
  814. this.$refs.previewFrame.srcdoc = previewDocument.documentElement.outerHTML;
  815. this.$refs.previewFrame.setAttribute('data-previewing-url', this.url);
  816. }
  817. }
  818. },
  819. setRouteQueryState() {
  820. const flat = key => this[key] !== '' ? `${key}=${this[key]}&` : ''
  821. const deep = key => {
  822. const haveItems = [...this[key]].length
  823. if (haveItems && this[key]['value'] !== '') {
  824. return `${key}=${JSON.stringify(this[key])}&`
  825. } else return ''
  826. }
  827. let flats = ['method', 'url', 'path', 'auth', 'httpUser', 'httpPassword', 'bearerToken', 'contentType'].map(item => flat(item))
  828. let deeps = ['headers', 'params'].map(item => deep(item))
  829. let bodyParams = this.rawInput ? [flat('rawParams')] : [deep('bodyParams')];
  830. this.$router.replace('/?' + flats.concat(deeps, bodyParams).join('').slice(0, -1))
  831. },
  832. setRouteQueries(queries) {
  833. if (typeof(queries) !== 'object') throw new Error('Route query parameters must be a Object')
  834. for (const key in queries) {
  835. if (key === 'headers' || key === 'params' || key === 'bodyParams') this[key] = JSON.parse(queries[key])
  836. if (key === 'rawParams') {
  837. this.rawInput = true
  838. this.rawParams = queries['rawParams']
  839. } else if (typeof(this[key]) === 'string') this[key] = queries[key]
  840. }
  841. },
  842. observeRequestButton() {
  843. const requestElement = this.$refs.request.$el;
  844. const sendButtonElement = this.$refs.sendButton;
  845. const observer = new IntersectionObserver((entries, observer) => {
  846. entries.forEach(entry => {
  847. sendButtonElement.classList.toggle('show');
  848. });
  849. }, {
  850. threshold: 1
  851. });
  852. observer.observe(requestElement);
  853. },
  854. handleImport () {
  855. let textarea = document.getElementById("import-text")
  856. let text = textarea.value;
  857. try {
  858. let parsedCurl = parseCurlCommand(text);
  859. this.url = parsedCurl.url.replace(/"/g,"").replace(/'/g,"");
  860. this.url = this.url[this.url.length -1] == '/' ? this.url.slice(0, -1): this.url;
  861. this.path = "";
  862. this.headers
  863. this.showModal = false;
  864. this.headers = [];
  865. for (const key of Object.keys(parsedCurl.headers)) {
  866. this.headers.push({
  867. key: key,
  868. value: parsedCurl.headers[key]
  869. })
  870. }
  871. this.method = parsedCurl.method.toUpperCase();
  872. } catch (error) {
  873. this.showModal = false;
  874. }
  875. },
  876. toggleModal() {
  877. this.showModal = !this.showModal;
  878. }
  879. },
  880. mounted() {
  881. this.observeRequestButton();
  882. },
  883. created() {
  884. if (Object.keys(this.$route.query).length) this.setRouteQueries(this.$route.query);
  885. this.$watch(vm => [
  886. vm.method,
  887. vm.url,
  888. vm.auth,
  889. vm.path,
  890. vm.httpUser,
  891. vm.httpPassword,
  892. vm.bearerToken,
  893. vm.headers,
  894. vm.params,
  895. vm.bodyParams,
  896. vm.contentType,
  897. vm.rawParams
  898. ], val => {
  899. this.setRouteQueryState()
  900. })
  901. }
  902. }
  903. </script>