refactor: init new state for body

This commit is contained in:
liyasthomas
2021-07-23 00:07:39 +05:30
parent 8597c04ff1
commit f694f1ad36
14 changed files with 336 additions and 166 deletions

74
components/http/Body.vue Normal file
View File

@@ -0,0 +1,74 @@
<template>
<div>
<div class="flex flex-1 py-2 items-center justify-between">
<tippy
ref="contentTypeOptions"
interactive
tabindex="-1"
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<div class="flex">
<span class="select-wrapper">
<input
id="contentType"
v-model="contentType"
class="
bg-primary
rounded-lg
flex
font-semibold font-mono
text-xs
w-full
py-2
px-4
transition
truncate
focus:outline-none
"
readonly
/>
</span>
</div>
</template>
<SmartItem
v-for="(contentTypeItem, index) in validContentTypes"
:key="`contentTypeItem-${index}`"
:label="contentTypeItem"
@click.native="
contentType = contentTypeItem
$refs.contentTypeOptions.tippy().hide()
"
/>
</tippy>
<SmartToggle :on="rawInput" class="px-4" @change="rawInput = !rawInput">
{{ $t("raw_input") }}
</SmartToggle>
</div>
<HttpBodyParameters v-if="!rawInput" :content-type="contentType" />
<HttpRawBody v-else :content-type="contentType" />
</div>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { pluckRef } from "~/helpers/utils/composables"
import { useRESTRequestBody } from "~/newstore/RESTSession"
import { knownContentTypes } from "~/helpers/utils/contenttypes"
export default defineComponent({
setup() {
return {
contentType: pluckRef(useRESTRequestBody(), "contentType"),
rawInput: pluckRef(useRESTRequestBody(), "isRaw"),
}
},
data() {
return {
validContentTypes: Object.keys(knownContentTypes),
}
},
})
</script>

View File

@@ -104,7 +104,7 @@
@click.native="toggleActive(index, param)" @click.native="toggleActive(index, param)"
/> />
</div> </div>
<div v-if="contentType === 'multipart/form-data'"> <div>
<label for="attachment" class="p-0"> <label for="attachment" class="p-0">
<ButtonSecondary <ButtonSecondary
class="w-full" class="w-full"
@@ -133,10 +133,15 @@
</AppSection> </AppSection>
</template> </template>
<script> <script lang="ts">
export default { import { defineComponent, toRef } from "@nuxtjs/composition-api"
props: { import { useRESTRequestBody } from "~/newstore/RESTSession"
bodyParams: { type: Array, default: () => [] },
export default defineComponent({
setup() {
return {
bodyParams: toRef(useRESTRequestBody(), "body"),
}
}, },
computed: { computed: {
contentType() { contentType() {
@@ -249,7 +254,7 @@ export default {
}) })
}, },
}, },
} })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -18,7 +18,7 @@
</label> </label>
<div> <div>
<ButtonSecondary <ButtonSecondary
v-if="rawInput && contentType.endsWith('json')" v-if="contentType.endsWith('json')"
ref="prettifyRequest" ref="prettifyRequest"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="$t('prettify_body')" :title="$t('prettify_body')"
@@ -66,43 +66,39 @@
</template> </template>
<script> <script>
import { defineComponent } from "@nuxtjs/composition-api"
import { getEditorLangForMimeType } from "~/helpers/editorutils" import { getEditorLangForMimeType } from "~/helpers/editorutils"
import { pluckRef } from "~/helpers/utils/composables"
import { useRESTRequestBody } from "~/newstore/RESTSession"
export default { export default defineComponent({
props: { props: {
rawParams: { type: String, default: null }, contentType: {
contentType: { type: String, default: null }, type: String,
rawInput: { type: Boolean, default: false }, required: true,
},
}, },
data() { setup() {
return { return {
rawParamsBody: pluckRef(useRESTRequestBody(), "body"),
prettifyIcon: "photo_filter", prettifyIcon: "photo_filter",
} }
}, },
computed: { computed: {
rawParamsBody: {
get() {
return this.rawParams
},
set(value) {
this.$emit("update-raw-body", value)
},
},
rawInputEditorLang() { rawInputEditorLang() {
return getEditorLangForMimeType(this.contentType) return getEditorLangForMimeType(this.contentType)
}, },
}, },
methods: { methods: {
clearContent(bodyParams, $event) { clearContent() {
this.$emit("clear-content", bodyParams, $event) this.rawParamsBody = ""
}, },
uploadPayload() { uploadPayload() {
this.$emit("update-raw-input", true)
const file = this.$refs.payload.files[0] const file = this.$refs.payload.files[0]
if (file !== undefined && file !== null) { if (file !== undefined && file !== null) {
const reader = new FileReader() const reader = new FileReader()
reader.onload = ({ target }) => { reader.onload = ({ target }) => {
this.$emit("update-raw-body", target.result) this.rawParamsBody = target.result
} }
reader.readAsText(file) reader.readAsText(file)
this.$toast.info(this.$t("file_imported"), { this.$toast.info(this.$t("file_imported"), {
@@ -128,5 +124,5 @@ export default {
} }
}, },
}, },
} })
</script> </script>

View File

@@ -0,0 +1,53 @@
<template>
<div>
<div v-if="results">
<div
v-for="(result, index) in results"
:key="`result-${index}`"
class="flex py-2 px-4 items-center"
>
<i
class="mr-4 material-icons"
:class="result.status === 'pass' ? 'text-green-500' : 'text-red-500'"
>
{{ result.status === "pass" ? "check_circle" : "cancel" }}
</i>
<span
v-if="result.message"
class="font-semibold text-secondaryDark text-xs"
>
{{ result.message }}
</span>
<span class="text-secondaryLight text-xs">
{{
` \xA0 — \xA0test ${
result.status === "pass" ? $t("passed") : $t("failed")
}`
}}
</span>
</div>
</div>
<div v-if="results.tests">
<HttpTestResult
v-for="(result, index) in results.expectResults"
:key="`result-${index}`"
class="divide-y divide-dividerLight"
:results="results.expectResults"
/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "@vue/composition-api"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
export default defineComponent({
props: {
results: {
type: Array as PropType<HoppTestResult>,
default: null,
},
},
})
</script>

View File

@@ -36,42 +36,45 @@
}" }"
complete-mode="test" complete-mode="test"
/> />
<div v-if="testReports.length !== 0"> <pre>
{{ testResults }}
</pre>
<div v-if="testResults">
<div class="flex flex-1 pl-4 items-center justify-between"> <div class="flex flex-1 pl-4 items-center justify-between">
<label class="font-semibold text-xs"> Test Reports </label> <div>
<label class="font-semibold text-xs"> Test Report: </label>
<span class="font-semibold text-xs text-red-500">
{{ failedTests }} failing,
</span>
<span class="font-semibold text-xs text-green-500">
{{ passedTests }} successful,
</span>
<span class="font-semibold text-xs text-secondaryDark">
out of {{ totalTests }} tests.
</span>
</div>
<ButtonSecondary <ButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="$t('clear')" :title="$t('clear')"
icon="clear_all" icon="clear_all"
@click.native="clearContent('tests', $event)" @click.native="clearContent()"
/> />
</div> </div>
<div <div v-if="testResults.expectResults">
v-for="(testReport, index) in testReports" <HttpTestResult
:key="`testReport-${index}`" v-for="(result, index) in testResults.expectResults"
class="px-4" :key="`result-${index}`"
> class="divide-y divide-dividerLight"
<div v-if="testReport.startBlock"> :results="testResults.expectResults"
<hr /> />
<h4 class="heading"> </div>
{{ testReport.startBlock }} <div v-if="testResults.tests">
</h4> <HttpTestResult
</div> v-for="(result, index) in testResults.tests"
<p :key="`result-${index}`"
v-else-if="testReport.result" class="divide-y divide-dividerLight"
class="flex font-mono flex-1 text-xs info" :results="testResults.tests"
> />
<span :class="testReport.styles.class" class="flex items-center">
<i class="text-sm material-icons">
{{ testReport.styles.icon }}
</i>
<span>&nbsp;{{ testReport.result }}</span>
<span v-if="testReport.message">
<label>: {{ testReport.message }}</label>
</span>
</span>
</p>
<div v-else-if="testReport.endBlock"><hr /></div>
</div> </div>
</div> </div>
</AppSection> </AppSection>
@@ -79,7 +82,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api" import { defineComponent } from "@nuxtjs/composition-api"
import { useTestScript, restTestResults$ } from "~/newstore/RESTSession" import {
useTestScript,
restTestResults$,
setRESTTestResults,
} from "~/newstore/RESTSession"
import { useReadonlyStream } from "~/helpers/utils/composables" import { useReadonlyStream } from "~/helpers/utils/composables"
export default defineComponent({ export default defineComponent({
@@ -87,8 +94,27 @@ export default defineComponent({
return { return {
testScript: useTestScript(), testScript: useTestScript(),
testResults: useReadonlyStream(restTestResults$, null), testResults: useReadonlyStream(restTestResults$, null),
testReports: [],
} }
}, },
computed: {
totalTests() {
return this.testResults.expectResults.length
},
failedTests() {
return this.testResults.expectResults.filter(
(result) => result.status === "fail"
).length
},
passedTests() {
return this.testResults.expectResults.filter(
(result) => result.status === "pass"
).length
},
},
methods: {
clearContent() {
setRESTTestResults(null)
},
},
}) })
</script> </script>

View File

@@ -177,7 +177,7 @@
</Splitpanes> </Splitpanes>
</template> </template>
<script lang="ts"> <script>
import { defineComponent } from "@nuxtjs/composition-api" import { defineComponent } from "@nuxtjs/composition-api"
import { Splitpanes, Pane } from "splitpanes" import { Splitpanes, Pane } from "splitpanes"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics" import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"

View File

@@ -1,3 +1,5 @@
import { ValidContentTypes } from "../utils/contenttypes"
export const RESTReqSchemaVersion = "1" export const RESTReqSchemaVersion = "1"
export type HoppRESTParam = { export type HoppRESTParam = {
@@ -12,6 +14,12 @@ export type HoppRESTHeader = {
active: boolean active: boolean
} }
export type HoppRESTReqBody = {
contentType: ValidContentTypes
body: string
isRaw: boolean
}
export interface HoppRESTRequest { export interface HoppRESTRequest {
v: string v: string
@@ -21,6 +29,8 @@ export interface HoppRESTRequest {
headers: HoppRESTHeader[] headers: HoppRESTHeader[]
preRequestScript: string preRequestScript: string
testScript: string testScript: string
body: HoppRESTReqBody
} }
export function makeRESTRequest( export function makeRESTRequest(
@@ -36,6 +46,22 @@ export function isHoppRESTRequest(x: any): x is HoppRESTRequest {
return x && typeof x === "object" && "v" in x return x && typeof x === "object" && "v" in x
} }
function parseRequestBody(x: any): HoppRESTReqBody {
if (x.contentType === "application/json") {
return {
contentType: "application/json",
body: x.rawParams,
isRaw: x.rawInput,
}
}
return {
contentType: "application/json",
body: "",
isRaw: x.rawInput,
}
}
export function translateToNewRequest(x: any): HoppRESTRequest { export function translateToNewRequest(x: any): HoppRESTRequest {
if (isHoppRESTRequest(x)) { if (isHoppRESTRequest(x)) {
return x return x
@@ -59,6 +85,8 @@ export function translateToNewRequest(x: any): HoppRESTRequest {
const preRequestScript = x.preRequestScript const preRequestScript = x.preRequestScript
const testScript = x.testScript const testScript = x.testScript
const body = parseRequestBody(x)
const result: HoppRESTRequest = { const result: HoppRESTRequest = {
endpoint, endpoint,
headers, headers,
@@ -66,6 +94,7 @@ export function translateToNewRequest(x: any): HoppRESTRequest {
method, method,
preRequestScript, preRequestScript,
testScript, testScript,
body,
v: RESTReqSchemaVersion, v: RESTReqSchemaVersion,
} }

View File

@@ -4,6 +4,7 @@ import {
readonly, readonly,
Ref, Ref,
ref, ref,
watch,
} from "@nuxtjs/composition-api" } from "@nuxtjs/composition-api"
import { Observable, Subscription } from "rxjs" import { Observable, Subscription } from "rxjs"
@@ -58,3 +59,28 @@ export function useStream<T>(
} }
}) })
} }
export function pluckRef<T, K extends keyof T>(ref: Ref<T>, key: K): Ref<T[K]> {
return customRef((track, trigger) => {
const stopWatching = watch(ref, (newVal, oldVal) => {
if (newVal[key] !== oldVal[key]) {
trigger()
}
})
onBeforeUnmount(() => {
stopWatching()
})
return {
get() {
track()
return ref.value[key]
},
set(value: T[K]) {
trigger()
ref.value = Object.assign(ref.value, { [key]: value })
},
}
})
}

View File

@@ -1,15 +0,0 @@
export const knownContentTypes = [
"application/json",
"application/ld+json",
"application/hal+json",
"application/vnd.api+json",
"application/xml",
"application/x-www-form-urlencoded",
"multipart/form-data",
"text/html",
"text/plain",
]
export function isJSONContentType(contentType) {
return /\bjson\b/i.test(contentType)
}

View File

@@ -0,0 +1,13 @@
export const knownContentTypes = {
"application/json": "json",
"application/ld+json": "json",
"application/hal+json": "json",
"application/vnd.api+json": "json",
"application/xml": "xml",
"application/x-www-form-urlencoded": "multipart",
"multipart/form-data": "multipart",
"text/html": "html",
"text/plain": "plain",
}
export type ValidContentTypes = keyof typeof knownContentTypes

View File

@@ -4,6 +4,7 @@ import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { import {
HoppRESTHeader, HoppRESTHeader,
HoppRESTParam, HoppRESTParam,
HoppRESTReqBody,
HoppRESTRequest, HoppRESTRequest,
RESTReqSchemaVersion, RESTReqSchemaVersion,
} from "~/helpers/types/HoppRESTRequest" } from "~/helpers/types/HoppRESTRequest"
@@ -127,6 +128,11 @@ const defaultRESTSession: RESTSession = {
method: "GET", method: "GET",
preRequestScript: "// pw.env.set('variable', 'value');", preRequestScript: "// pw.env.set('variable', 'value');",
testScript: "// pw.expect('variable').toBe('value');", testScript: "// pw.expect('variable').toBe('value');",
body: {
contentType: "application/json",
body: "",
isRaw: false,
},
}, },
response: null, response: null,
testResults: null, testResults: null,
@@ -310,6 +316,14 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
setRequestBody(curr: RESTSession, { newBody }: { newBody: HoppRESTReqBody }) {
return {
request: {
...curr.request,
body: newBody,
},
}
},
updateResponse( updateResponse(
_curr: RESTSession, _curr: RESTSession,
{ updatedRes }: { updatedRes: HoppRESTResponse | null } { updatedRes }: { updatedRes: HoppRESTResponse | null }
@@ -454,6 +468,15 @@ export function setRESTTestScript(newScript: string) {
}) })
} }
export function setRESTReqBody(newBody: HoppRESTReqBody | null) {
restSessionStore.dispatch({
dispatcher: "setRequestBody",
payload: {
newBody,
},
})
}
export function updateRESTResponse(updatedRes: HoppRESTResponse | null) { export function updateRESTResponse(updatedRes: HoppRESTResponse | null) {
restSessionStore.dispatch({ restSessionStore.dispatch({
dispatcher: "updateResponse", dispatcher: "updateResponse",
@@ -522,6 +545,11 @@ export const restTestScript$ = restSessionStore.subject$.pipe(
distinctUntilChanged() distinctUntilChanged()
) )
export const restReqBody$ = restSessionStore.subject$.pipe(
pluck("request", "body"),
distinctUntilChanged()
)
export const restResponse$ = restSessionStore.subject$.pipe( export const restResponse$ = restSessionStore.subject$.pipe(
pluck("response"), pluck("response"),
distinctUntilChanged() distinctUntilChanged()
@@ -572,3 +600,11 @@ export function useTestScript(): Ref<string> {
} }
) )
} }
export function useRESTRequestBody(): Ref<HoppRESTReqBody> {
return useStream(
restReqBody$,
restSessionStore.value.request.body,
setRESTReqBody
)
}

19
package-lock.json generated
View File

@@ -63,6 +63,7 @@
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
"@types/lodash": "^4.14.171", "@types/lodash": "^4.14.171",
"@types/splitpanes": "^2.2.1",
"@vue/test-utils": "^1.2.1", "@vue/test-utils": "^1.2.1",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-jest": "^27.0.6", "babel-jest": "^27.0.6",
@@ -7389,6 +7390,15 @@
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==" "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA=="
}, },
"node_modules/@types/splitpanes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/splitpanes/-/splitpanes-2.2.1.tgz",
"integrity": "sha512-H5BgO6UdJRzz5ddRzuGvLBiPSPEuuHXb5ET+7avLLrEx1uc7f5Ut5oLMDQsfvGtHBBAFczt1QNYuDf27wHbvDQ==",
"dev": true,
"dependencies": {
"vue": "^2.0.0"
}
},
"node_modules/@types/stack-utils": { "node_modules/@types/stack-utils": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",
@@ -37392,6 +37402,15 @@
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==" "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA=="
}, },
"@types/splitpanes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/splitpanes/-/splitpanes-2.2.1.tgz",
"integrity": "sha512-H5BgO6UdJRzz5ddRzuGvLBiPSPEuuHXb5ET+7avLLrEx1uc7f5Ut5oLMDQsfvGtHBBAFczt1QNYuDf27wHbvDQ==",
"dev": true,
"requires": {
"vue": "^2.0.0"
}
},
"@types/stack-utils": { "@types/stack-utils": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz",

View File

@@ -79,6 +79,7 @@
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
"@types/lodash": "^4.14.171", "@types/lodash": "^4.14.171",
"@types/splitpanes": "^2.2.1",
"@vue/test-utils": "^1.2.1", "@vue/test-utils": "^1.2.1",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-jest": "^27.0.6", "babel-jest": "^27.0.6",

View File

@@ -17,77 +17,7 @@
</SmartTab> </SmartTab>
<SmartTab :id="'bodyParams'" :label="$t('body')" info="0"> <SmartTab :id="'bodyParams'" :label="$t('body')" info="0">
<div class="flex flex-1 py-2 items-center justify-between"> <HttpBody />
<tippy
interactive
ref="contentTypeOptions"
tabindex="-1"
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<div class="flex">
<span class="select-wrapper">
<input
id="contentType"
class="
bg-primary
rounded-lg
flex
font-semibold font-mono
text-xs
w-full
py-2
px-4
transition
truncate
focus:outline-none
"
v-model="contentType"
readonly
/>
</span>
</div>
</template>
<SmartItem
v-for="(contentTypeItem, index) in validContentTypes"
:key="`contentTypeItem-${index}`"
@click.native="
contentType = contentTypeItem
$refs.contentTypeOptions.tippy().hide()
"
:label="contentTypeItem"
/>
</tippy>
<SmartToggle
v-if="canListParameters"
:on="rawInput"
@change="rawInput = !rawInput"
class="px-4"
>
{{ $t("raw_input") }}
</SmartToggle>
</div>
<HttpBodyParameters
v-if="!rawInput"
:bodyParams="bodyParams"
@clear-content="clearContent"
@set-route-query-state="setRouteQueryState"
@remove-request-body-param="removeRequestBodyParam"
@add-request-body-param="addRequestBodyParam"
/>
<HttpRawBody
v-else
:rawParams="rawParams"
:contentType="contentType"
:rawInput="rawInput"
@clear-content="clearContent"
@update-raw-body="updateRawBody"
@update-raw-input="
updateRawInput = (value) => (rawInput = value)
"
/>
</SmartTab> </SmartTab>
<SmartTab <SmartTab
@@ -555,18 +485,6 @@ export default defineComponent({
} }
}, },
watch: { watch: {
canListParameters: {
immediate: true,
handler(canListParameters) {
if (canListParameters) {
this.$nextTick(() => {
this.rawInput = Boolean(this.rawParams && this.rawParams !== "{}")
})
} else {
this.rawInput = true
}
},
},
contentType(contentType, oldContentType) { contentType(contentType, oldContentType) {
const getDefaultParams = (contentType) => { const getDefaultParams = (contentType) => {
if (isJSONContentType(contentType)) return "{}" if (isJSONContentType(contentType)) return "{}"
@@ -632,17 +550,6 @@ export default defineComponent({
}, },
}, },
computed: { computed: {
/**
* Check content types that can be automatically
* serialized by Hoppscotch.
*/
canListParameters() {
return (
this.contentType === "application/x-www-form-urlencoded" ||
this.contentType === "multipart/form-data" ||
isJSONContentType(this.contentType)
)
},
uri: { uri: {
get() { get() {
return this.$store.state.request.uri return this.$store.state.request.uri