From 2972ac6328f46f592d33905ea0252aa3fbe8e38d Mon Sep 17 00:00:00 2001 From: Liyas Thomas Date: Fri, 19 Feb 2021 22:31:31 +0530 Subject: [PATCH] Support multipart/form-data content-type (#1485) * Initial UI refactor - move raw and key-value body to components and tabs * Delete package-lock.json * deps * Add multipart/form-data as a content type * fix: add default contentType value * Allow http body param request body with multipart/form-data * Add form data to vuex * move raw body components to 'Raw Request Body' tab * Add files addition logic * Set Dockerfile to run nuxt in dev mode * Set Dockerfile to run nuxt in dev mode * Draft version of file upload * refactor: clean up * Add file chip to denote file input * Remove console.log * refactor(ui): matching styles * refactor(ui): matching styles * fix(ui): mobile responsiveness * fix(ui): mobile responsiveness * refactor: minor cleanup * Remove file from any form of persistence * Add warning that form data files will not be saved to local storage * Add remove file functionality * Prevent file from being saved to collections * Remove console.log * fix active toggle on multipart/form-data + cleanup * auto import components Co-authored-by: nelsontky --- Dockerfile | 2 +- components/http/http-body-parameters.vue | 68 ++++++++++ components/http/http-raw-body.vue | 114 ++++++++++++++++ components/ui/deletable-chip.vue | 32 +++++ helpers/utils/contenttypes.js | 3 +- pages/index.vue | 166 ++++------------------- plugins/vuex-persist.js | 4 +- store/mutations.js | 12 ++ store/postwoman.js | 10 +- 9 files changed, 271 insertions(+), 140 deletions(-) create mode 100644 components/http/http-raw-body.vue create mode 100644 components/ui/deletable-chip.vue diff --git a/Dockerfile b/Dockerfile index 4443e3f66..eb6c1a5a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,4 +22,4 @@ RUN npm run generate ENV HOST 0.0.0.0 EXPOSE 3000 -CMD ["npm", "run", "start"] +CMD ["npm", "run", "dev"] diff --git a/components/http/http-body-parameters.vue b/components/http/http-body-parameters.vue index 5dd52baa9..e76e02cd0 100644 --- a/components/http/http-body-parameters.vue +++ b/components/http/http-body-parameters.vue @@ -39,9 +39,12 @@
  • +
    +
    + + {{ file.name }} + +
    +
  • @@ -80,6 +94,22 @@
  • +
    +
  • + + +
  • +
  • - - - - - -
  • @@ -159,36 +111,15 @@ @remove-request-body-param="removeRequestBodyParam" @add-request-body-param="addRequestBodyParam" /> -
    -
      -
    • -
      - -
      - -
      -
      - -
    • -
    -
    +
    @@ -725,7 +656,6 @@ import parseTemplateString from "~/helpers/templating" import { tokenRequest, oauthRedirect } from "~/helpers/oauth" import { cancelRunningRequest, sendNetworkRequest } from "~/helpers/network" import { fb } from "~/helpers/fb" -import { getEditorLangForMimeType } from "~/helpers/editorutils" import { hasPathParams, addPathParamsToVariables, getQueryParams } from "~/helpers/requestParams" import { parseUrlAndPath } from "~/helpers/utils/uri" import { httpValid } from "~/helpers/utils/valid" @@ -897,6 +827,7 @@ export default { canListParameters() { return ( this.contentType === "application/x-www-form-urlencoded" || + this.contentType === "multipart/form-data" || isJSONContentType(this.contentType) ) }, @@ -1115,9 +1046,6 @@ export default { this.$store.commit("setState", { value, attribute: "rawInput" }) }, }, - rawInputEditorLang() { - return getEditorLangForMimeType(this.contentType) - }, requestType: { get() { return this.$store.state.request.requestType @@ -1314,7 +1242,10 @@ export default { let environmentVariables = getEnvironmentVariablesFromScript(preRequestScript) environmentVariables = addPathParamsToVariables(this.params, environmentVariables) requestOptions.url = parseTemplateString(requestOptions.url, environmentVariables) - requestOptions.data = parseTemplateString(requestOptions.data, environmentVariables) + if (!(requestOptions.data instanceof FormData)) { + // TODO: Parse env variables for form data too + requestOptions.data = parseTemplateString(requestOptions.data, environmentVariables) + } for (let k in requestOptions.headers) { const kParsed = parseTemplateString(k, environmentVariables) const valParsed = parseTemplateString(requestOptions.headers[k], environmentVariables) @@ -1370,13 +1301,19 @@ export default { }) } requestBody = requestBody ? requestBody.toString() : null - if (this.files.length !== 0) { + if (this.contentType === "multipart/form-data") { const formData = new FormData() - for (let i = 0; i < this.files.length; i++) { - let file = this.files[i] - formData.append(i, file) + for (const bodyParam of this.bodyParams.filter((item) => + item.hasOwnProperty("active") ? item.active == true : true + )) { + if (bodyParam?.value?.[0] instanceof File) { + for (const file of bodyParam.value) { + formData.append(bodyParam.key, file) + } + } else { + formData.append(bodyParam.key, bodyParam.value) + } } - formData.append("data", requestBody) requestBody = formData } // If the request uses a token for auth, we want to make sure it's sent here. @@ -1645,19 +1582,6 @@ export default { }, }) }, - prettifyRequestBody() { - try { - const jsonObj = JSON.parse(this.rawParams) - this.rawParams = JSON.stringify(jsonObj, null, 2) - let oldIcon = this.$refs.prettifyRequest.innerHTML - this.$refs.prettifyRequest.innerHTML = this.doneButton - setTimeout(() => (this.$refs.prettifyRequest.innerHTML = oldIcon), 1000) - } catch (e) { - this.$toast.error(`${this.$t("json_prettify_invalid_body")}`, { - icon: "error", - }) - } - }, copyRequest() { if (navigator.share) { const time = new Date().toLocaleTimeString() @@ -1689,7 +1613,9 @@ export default { const deep = (key) => { const haveItems = [...this[key]].length if (haveItems && this[key]["value"] !== "") { - return `${key}=${JSON.stringify(this[key])}&` + // Exclude files fro query params + const filesRemoved = this[key].filter((item) => !(item?.value?.[0] instanceof File)) + return `${key}=${JSON.stringify(filesRemoved)}&` } return "" } @@ -1896,40 +1822,8 @@ export default { } this.setRouteQueryState() }, - uploadAttachment() { - this.filenames = "" - this.files = this.$refs.attachment.files - if (this.files.length !== 0) { - for (let file of this.files) { - this.filenames = `${this.filenames}
    ${file.name}` - } - this.$toast.info(this.$t("file_imported"), { - icon: "attach_file", - }) - } else { - this.$toast.error(this.$t("choose_file"), { - icon: "attach_file", - }) - } - }, - uploadPayload() { - this.rawInput = true - const file = this.$refs.payload.files[0] - if (file !== undefined && file !== null) { - const reader = new FileReader() - reader.onload = ({ target }) => { - this.rawParams = target.result - } - reader.readAsText(file) - this.$toast.info(this.$t("file_imported"), { - icon: "attach_file", - }) - } else { - this.$toast.error(this.$t("choose_file"), { - icon: "attach_file", - }) - } - this.$refs.payload.value = "" + updateRawBody(rawParams) { + this.rawParams = rawParams }, async handleAccessTokenRequest() { if (this.oidcDiscoveryUrl === "" && (this.authUrl === "" || this.accessTokenUrl === "")) { diff --git a/plugins/vuex-persist.js b/plugins/vuex-persist.js index c834193f6..0dd9869a4 100644 --- a/plugins/vuex-persist.js +++ b/plugins/vuex-persist.js @@ -1,5 +1,7 @@ import VuexPersistence from "vuex-persist" export default ({ store }) => { - new VuexPersistence().plugin(store) + new VuexPersistence({ + filter: (mutation) => mutation.type !== "setFilesBodyParams", + }).plugin(store) } diff --git a/store/mutations.js b/store/mutations.js index aa692673d..ad5f37b47 100644 --- a/store/mutations.js +++ b/store/mutations.js @@ -107,6 +107,18 @@ export default { request.bodyParams[index].value = value }, + // While this mutation is same as the setValueBodyParams above, it is excluded + // from vuex-persist. We will commit this mutation while adding a file + // param as there is no way to serialize File objects and thus we cannot + // persist file objects in localStorage + setFilesBodyParams({ request }, { index, value }) { + request.bodyParams[index].value = value + }, + + removeFile({ request }, { index, fileIndex }) { + request.bodyParams[index].value.splice(fileIndex, 1) + }, + setActiveBodyParams({ request }, { index, value }) { if (!request.bodyParams[index].hasOwnProperty("active")) { Vue.set(request.bodyParams[index], "active", value) diff --git a/store/postwoman.js b/store/postwoman.js index 4f055bcb2..760398f96 100644 --- a/store/postwoman.js +++ b/store/postwoman.js @@ -264,7 +264,15 @@ export const mutations = { }, saveRequestAs({ collections }, payload) { - const { request, collectionIndex, folderName, requestIndex } = payload + let { request, collectionIndex, folderName, requestIndex } = payload + + // Filter out all file inputs + request = { + ...request, + bodyParams: request.bodyParams.map((param) => + param?.value?.[0] instanceof File ? { ...param, value: "" } : param + ), + } const specifiedCollection = collectionIndex !== undefined const specifiedFolder = folderName !== undefined