2771 lines
91 KiB
Vue
2771 lines
91 KiB
Vue
<template>
|
|
<div class="page">
|
|
<div class="content">
|
|
<div class="page-columns inner-left">
|
|
<pw-section v-if="showPreRequestScript" class="orange" label="Pre-Request" ref="preRequest">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="generatedCode">{{ $t('javascript_code') }}</label>
|
|
<div>
|
|
<a
|
|
href="https://github.com/liyasthomas/postwoman/wiki/Pre-Request-Scripts"
|
|
target="_blank"
|
|
rel="noopener"
|
|
>
|
|
<button class="icon" v-tooltip="$t('wiki')">
|
|
<i class="material-icons">help</i>
|
|
</button>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<Editor
|
|
v-model="preRequestScript"
|
|
:lang="'javascript'"
|
|
:options="{
|
|
maxLines: '16',
|
|
minLines: '8',
|
|
fontSize: '16px',
|
|
autoScrollEditorIntoView: true,
|
|
showPrintMargin: false,
|
|
useWorker: false,
|
|
}"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
</pw-section>
|
|
|
|
<pw-section class="blue" :label="$t('request')" ref="request">
|
|
<ul>
|
|
<li>
|
|
<label for="method">{{ $t('method') }}</label>
|
|
<span class="select-wrapper">
|
|
<v-popover>
|
|
<input
|
|
id="method"
|
|
class="method"
|
|
v-if="!customMethod"
|
|
v-model="method"
|
|
readonly
|
|
/>
|
|
<input v-else v-model="method" placeholder="CUSTOM" />
|
|
<template slot="popover">
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = false
|
|
method = 'GET'
|
|
"
|
|
v-close-popover
|
|
>
|
|
GET
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = false
|
|
method = 'HEAD'
|
|
"
|
|
v-close-popover
|
|
>
|
|
HEAD
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = false
|
|
method = 'POST'
|
|
"
|
|
v-close-popover
|
|
>
|
|
POST
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = false
|
|
method = 'PUT'
|
|
"
|
|
v-close-popover
|
|
>
|
|
PUT
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = false
|
|
method = 'DELETE'
|
|
"
|
|
v-close-popover
|
|
>
|
|
DELETE
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = false
|
|
method = 'CONNECT'
|
|
"
|
|
v-close-popover
|
|
>
|
|
CONNECT
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = false
|
|
method = 'OPTIONS'
|
|
"
|
|
v-close-popover
|
|
>
|
|
OPTIONS
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = false
|
|
method = 'TRACE'
|
|
"
|
|
v-close-popover
|
|
>
|
|
TRACE
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = false
|
|
method = 'PATCH'
|
|
"
|
|
v-close-popover
|
|
>
|
|
PATCH
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="
|
|
customMethod = true
|
|
method = 'CUSTOM'
|
|
"
|
|
v-close-popover
|
|
>
|
|
CUSTOM
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</v-popover>
|
|
</span>
|
|
</li>
|
|
<li>
|
|
<label for="url">{{ $t('url') }}</label>
|
|
<input
|
|
:class="{ error: !isValidURL }"
|
|
@keyup.enter="isValidURL ? sendRequest() : null"
|
|
id="url"
|
|
name="url"
|
|
type="url"
|
|
v-model="url"
|
|
/>
|
|
</li>
|
|
<li>
|
|
<label for="path">{{ $t('path') }}</label>
|
|
<input
|
|
@keyup.enter="isValidURL ? sendRequest() : null"
|
|
id="path"
|
|
name="path"
|
|
v-model="path"
|
|
@input="pathInputHandler"
|
|
/>
|
|
</li>
|
|
<li>
|
|
<label for="label">{{ $t('label') }}</label>
|
|
<input
|
|
id="label"
|
|
name="label"
|
|
type="text"
|
|
v-model="label"
|
|
:placeholder="$t('optional')"
|
|
/>
|
|
</li>
|
|
<li>
|
|
<label class="hide-on-small-screen" for="send"> </label>
|
|
<button :disabled="!isValidURL" @click="sendRequest" id="send" ref="sendButton">
|
|
{{ $t('send') }}
|
|
<span>
|
|
<i class="material-icons">send</i>
|
|
</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
<div class="blue" label="Request Body" v-if="['POST', 'PUT', 'PATCH'].includes(method)">
|
|
<ul>
|
|
<li>
|
|
<label for="contentType">{{ $t('content_type') }}</label>
|
|
<autocomplete :source="validContentTypes" :spellcheck="false" v-model="contentType"
|
|
>Content Type</autocomplete
|
|
>
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<span>
|
|
<pw-toggle :on="rawInput" @change="rawInput = $event">
|
|
{{ $t('raw_input') }}
|
|
</pw-toggle>
|
|
</span>
|
|
<div>
|
|
<label for="attachment">
|
|
<button
|
|
class="icon"
|
|
@click="$refs.attachment.click()"
|
|
v-tooltip="
|
|
files.length === 0 ? $t('upload_file') : filenames.replace('<br/>', '')
|
|
"
|
|
>
|
|
<i class="material-icons">attach_file</i>
|
|
<span>
|
|
{{
|
|
files.length === 0
|
|
? 'No files'
|
|
: files.length == 1
|
|
? '1 file'
|
|
: files.length + ' files'
|
|
}}
|
|
</span>
|
|
</button>
|
|
</label>
|
|
<input
|
|
ref="attachment"
|
|
name="attachment"
|
|
type="file"
|
|
@change="uploadAttachment"
|
|
multiple
|
|
/>
|
|
<label for="payload">
|
|
<button
|
|
class="icon"
|
|
@click="$refs.payload.click()"
|
|
v-tooltip="$t('import_json')"
|
|
>
|
|
<i class="material-icons">post_add</i>
|
|
</button>
|
|
</label>
|
|
<input ref="payload" name="payload" type="file" @change="uploadPayload" />
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<div v-if="!rawInput">
|
|
<ul>
|
|
<li>
|
|
<label for="reqParamList">{{ $t('parameter_list') }}</label>
|
|
<textarea
|
|
id="reqParamList"
|
|
readonly
|
|
v-textarea-auto-height="rawRequestBody"
|
|
v-model="rawRequestBody"
|
|
:placeholder="$t('add_one_parameter')"
|
|
rows="1"
|
|
></textarea>
|
|
</li>
|
|
</ul>
|
|
<ul v-for="(param, index) in bodyParams" :key="index">
|
|
<li>
|
|
<input
|
|
:placeholder="'key ' + (index + 1)"
|
|
:name="'bparam' + index"
|
|
:value="param.key"
|
|
@change="
|
|
$store.commit('setKeyBodyParams', {
|
|
index,
|
|
value: $event.target.value,
|
|
})
|
|
"
|
|
@keyup.prevent="setRouteQueryState"
|
|
autofocus
|
|
/>
|
|
</li>
|
|
<li>
|
|
<input
|
|
:placeholder="'value ' + (index + 1)"
|
|
:id="'bvalue' + index"
|
|
:name="'bvalue' + index"
|
|
:value="param.value"
|
|
@change="
|
|
$store.commit('setValueBodyParams', {
|
|
index,
|
|
value: $event.target.value,
|
|
})
|
|
"
|
|
@keyup.prevent="setRouteQueryState"
|
|
/>
|
|
</li>
|
|
<div>
|
|
<li>
|
|
<button
|
|
class="icon"
|
|
@click="removeRequestBodyParam(index)"
|
|
v-tooltip.bottom="$t('delete')"
|
|
id="delParam"
|
|
>
|
|
<i class="material-icons">delete</i>
|
|
</button>
|
|
</li>
|
|
</div>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<button class="icon" @click="addRequestBodyParam" name="addrequest">
|
|
<i class="material-icons">add</i>
|
|
<span>{{ $t('add_new') }}</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div v-else>
|
|
<ul>
|
|
<li>
|
|
<label for="rawBody">{{ $t('raw_request_body') }}</label>
|
|
<Editor
|
|
v-model="rawParams"
|
|
:lang="rawInputEditorLang"
|
|
:options="{
|
|
maxLines: '16',
|
|
minLines: '8',
|
|
fontSize: '16px',
|
|
autoScrollEditorIntoView: true,
|
|
showPrintMargin: false,
|
|
useWorker: false,
|
|
}"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="flex-wrap">
|
|
<span>
|
|
<button
|
|
class="icon"
|
|
id="show-modal"
|
|
@click="showModal = true"
|
|
v-tooltip.bottom="$t('import_curl')"
|
|
>
|
|
<i class="material-icons">import_export</i>
|
|
</button>
|
|
<button
|
|
class="icon"
|
|
id="code"
|
|
@click="isHidden = !isHidden"
|
|
:disabled="!isValidURL"
|
|
v-tooltip.bottom="{
|
|
content: isHidden ? $t('show_code') : $t('hide_code'),
|
|
}"
|
|
>
|
|
<i class="material-icons">code</i>
|
|
</button>
|
|
<button
|
|
class="icon"
|
|
id="preRequestScriptButton"
|
|
v-tooltip.bottom="{
|
|
content: !showPreRequestScript
|
|
? $t('show_prerequest_script')
|
|
: $t('hide_prerequest_script'),
|
|
}"
|
|
@click="showPreRequestScript = !showPreRequestScript"
|
|
>
|
|
<i
|
|
class="material-icons"
|
|
:class="showPreRequestScript"
|
|
v-if="!showPreRequestScript"
|
|
>
|
|
playlist_add
|
|
</i>
|
|
<i class="material-icons" :class="showPreRequestScript" v-else>
|
|
close
|
|
</i>
|
|
</button>
|
|
<button
|
|
class="icon"
|
|
id="preRequestScriptButto"
|
|
v-tooltip.bottom="{
|
|
content: !testsEnabled ? 'Enable Tests' : 'Disable Tests',
|
|
}"
|
|
@click="testsEnabled = !testsEnabled"
|
|
>
|
|
<i class="material-icons" :class="testsEnabled" v-if="!testsEnabled">
|
|
playlist_add_check
|
|
</i>
|
|
<i class="material-icons" :class="testsEnabled" v-else>close</i>
|
|
</button>
|
|
</span>
|
|
<span>
|
|
<button
|
|
class="icon"
|
|
@click="copyRequest"
|
|
id="copyRequest"
|
|
ref="copyRequest"
|
|
:disabled="!isValidURL"
|
|
v-tooltip.bottom="$t('copy_request_link')"
|
|
>
|
|
<i v-if="navigatorShare" class="material-icons">share</i>
|
|
<i v-else class="material-icons">file_copy</i>
|
|
</button>
|
|
<button
|
|
class="icon"
|
|
@click="saveRequest"
|
|
id="saveRequest"
|
|
ref="saveRequest"
|
|
:disabled="!isValidURL"
|
|
v-tooltip.bottom="$t('save_to_collections')"
|
|
>
|
|
<i class="material-icons">save</i>
|
|
</button>
|
|
<button
|
|
class="icon"
|
|
@click="clearContent('', $event)"
|
|
v-tooltip.bottom="$t('clear_all')"
|
|
ref="clearAll"
|
|
>
|
|
<i class="material-icons">clear_all</i>
|
|
</button>
|
|
</span>
|
|
</div>
|
|
</pw-section>
|
|
|
|
<pw-section v-if="testsEnabled" class="orange" label="Tests" ref="postRequestTests">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="generatedCode">{{ $t('javascript_code') }}</label>
|
|
<div>
|
|
<a
|
|
href="https://github.com/liyasthomas/postwoman/wiki/Post-Requests-Tests"
|
|
target="_blank"
|
|
rel="noopener"
|
|
>
|
|
<button class="icon" v-tooltip="$t('wiki')">
|
|
<i class="material-icons">help</i>
|
|
</button>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<Editor
|
|
v-model="testScript"
|
|
:lang="'javascript'"
|
|
:options="{
|
|
maxLines: '16',
|
|
minLines: '8',
|
|
fontSize: '16px',
|
|
autoScrollEditorIntoView: true,
|
|
showPrintMargin: false,
|
|
useWorker: false,
|
|
}"
|
|
/>
|
|
<div v-if="testReports">
|
|
<div class="flex-wrap">
|
|
<label>Test Reports</label>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="clearContent('tests', $event)"
|
|
v-tooltip.bottom="$t('clear')"
|
|
>
|
|
<i class="material-icons">clear_all</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div v-for="(testReport, index) in testReports" :key="index">
|
|
<div v-if="testReport.startBlock" class="info">
|
|
<h4>{{ testReport.startBlock }}</h4>
|
|
</div>
|
|
<p v-else-if="testReport.result" class="flex-wrap info">
|
|
<span :class="testReport.styles.class">
|
|
<i class="material-icons">
|
|
{{ testReport.styles.icon }}
|
|
</i>
|
|
<span> {{ 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>
|
|
</li>
|
|
</ul>
|
|
</pw-section>
|
|
<section id="options">
|
|
<input id="tab-one" type="radio" name="options" checked="checked" />
|
|
<label for="tab-one">{{ $t('authentication') }}</label>
|
|
<div class="tab">
|
|
<pw-section class="cyan" :label="$t('authentication')" ref="authentication">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="auth">{{ $t('authentication') }}</label>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="clearContent('auth', $event)"
|
|
v-tooltip.bottom="$t('clear')"
|
|
>
|
|
<i class="material-icons">clear_all</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<span class="select-wrapper">
|
|
<select id="auth" v-model="auth">
|
|
<option>None</option>
|
|
<option>Basic Auth</option>
|
|
<option>Bearer Token</option>
|
|
<option>OAuth 2.0</option>
|
|
</select>
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
<ul v-if="auth === 'Basic Auth'">
|
|
<li>
|
|
<input placeholder="User" name="http_basic_user" v-model="httpUser" />
|
|
</li>
|
|
<li>
|
|
<input
|
|
placeholder="Password"
|
|
name="http_basic_passwd"
|
|
:type="passwordFieldType"
|
|
v-model="httpPassword"
|
|
/>
|
|
</li>
|
|
<div>
|
|
<li>
|
|
<button
|
|
class="icon"
|
|
id="switchVisibility"
|
|
ref="switchVisibility"
|
|
@click="switchVisibility"
|
|
>
|
|
<i class="material-icons" v-if="passwordFieldType === 'text'">visibility</i>
|
|
<i class="material-icons" v-if="passwordFieldType !== 'text'"
|
|
>visibility_off</i
|
|
>
|
|
</button>
|
|
</li>
|
|
</div>
|
|
</ul>
|
|
<ul v-if="auth === 'Bearer Token' || auth === 'OAuth 2.0'">
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<input placeholder="Token" name="bearer_token" v-model="bearerToken" />
|
|
<button
|
|
v-if="auth === 'OAuth 2.0'"
|
|
class="icon"
|
|
@click="showTokenList = !showTokenList"
|
|
v-tooltip.bottom="$t('use_token')"
|
|
>
|
|
<i class="material-icons">open_in_new</i>
|
|
</button>
|
|
<button
|
|
v-if="auth === 'OAuth 2.0'"
|
|
class="icon"
|
|
@click="showTokenRequest = !showTokenRequest"
|
|
v-tooltip.bottom="$t('get_token')"
|
|
>
|
|
<i class="material-icons">vpn_key</i>
|
|
</button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<div class="flex-wrap">
|
|
<pw-toggle :on="!urlExcludes.auth" @change="setExclude('auth', !$event)">
|
|
{{ $t('include_in_url') }}
|
|
</pw-toggle>
|
|
</div>
|
|
</pw-section>
|
|
<pw-section
|
|
v-if="showTokenRequest"
|
|
class="red"
|
|
label="Access Token Request"
|
|
ref="accessTokenRequest"
|
|
>
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="token-name">{{ $t('token_name') }}</label>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="showTokenRequestList = true"
|
|
v-tooltip.bottom="$t('manage_token_req')"
|
|
>
|
|
<i class="material-icons">library_add</i>
|
|
</button>
|
|
<button
|
|
class="icon"
|
|
@click="clearContent('access_token', $event)"
|
|
v-tooltip.bottom="$t('clear')"
|
|
>
|
|
<i class="material-icons">clear_all</i>
|
|
</button>
|
|
<button
|
|
class="icon"
|
|
@click="showTokenRequest = false"
|
|
v-tooltip.bottom="$t('close')"
|
|
>
|
|
<i class="material-icons">close</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<input
|
|
id="token-name"
|
|
:placeholder="$t('optional')"
|
|
name="token_name"
|
|
v-model="accessTokenName"
|
|
type="text"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<label for="oidc-discovery-url">
|
|
{{ $t('oidc_discovery_url') }}
|
|
</label>
|
|
<input
|
|
:disabled="this.authUrl !== '' || this.accessTokenUrl !== ''"
|
|
id="oidc-discovery-url"
|
|
name="oidc_discovery_url"
|
|
type="url"
|
|
v-model="oidcDiscoveryUrl"
|
|
placeholder="https://example.com/.well-known/openid-configuration"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<label for="auth-url">{{ $t('auth_url') }}</label>
|
|
<input
|
|
:disabled="this.oidcDiscoveryUrl !== ''"
|
|
id="auth-url"
|
|
name="auth_url"
|
|
type="url"
|
|
v-model="authUrl"
|
|
placeholder="https://example.com/login/oauth/authorize"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<label for="access-token-url">
|
|
{{ $t('access_token_url') }}
|
|
</label>
|
|
<input
|
|
:disabled="this.oidcDiscoveryUrl !== ''"
|
|
id="access-token-url"
|
|
name="access_token_url"
|
|
type="url"
|
|
v-model="accessTokenUrl"
|
|
placeholder="https://example.com/login/oauth/access_token"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<label for="client-id">{{ $t('client_id') }}</label>
|
|
<input
|
|
id="client-id"
|
|
name="client_id"
|
|
type="text"
|
|
v-model="clientId"
|
|
placeholder="Client ID"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<label for="scope">{{ $t('scope') }}</label>
|
|
<input
|
|
id="scope"
|
|
name="scope"
|
|
type="text"
|
|
v-model="scope"
|
|
placeholder="e.g. read:org"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<button class="icon" @click="handleAccessTokenRequest">
|
|
<i class="material-icons">vpn_key</i>
|
|
<span>{{ $t('request_token') }}</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</pw-section>
|
|
</div>
|
|
<input id="tab-two" type="radio" name="options" />
|
|
<label for="tab-two">{{ $t('headers') }}</label>
|
|
<div class="tab">
|
|
<pw-section class="orange" label="Headers" ref="headers">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="headerList">{{ $t('header_list') }}</label>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="clearContent('headers', $event)"
|
|
v-tooltip.bottom="$t('clear')"
|
|
>
|
|
<i class="material-icons">clear_all</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<textarea
|
|
id="headerList"
|
|
readonly
|
|
v-textarea-auto-height="headerString"
|
|
v-model="headerString"
|
|
:placeholder="$t('add_one_header')"
|
|
rows="1"
|
|
></textarea>
|
|
</li>
|
|
</ul>
|
|
<ul v-for="(header, index) in headers" :key="index">
|
|
<li>
|
|
<autocomplete
|
|
:placeholder="$t('header_count', { count: index + 1 })"
|
|
:source="commonHeaders"
|
|
:spellcheck="false"
|
|
:value="header.key"
|
|
@input="
|
|
$store.commit('setKeyHeader', {
|
|
index,
|
|
value: $event,
|
|
})
|
|
"
|
|
@keyup.prevent="setRouteQueryState"
|
|
autofocus
|
|
/>
|
|
</li>
|
|
<li>
|
|
<input
|
|
:placeholder="$t('value_count', { count: index + 1 })"
|
|
:name="'value' + index"
|
|
:value="header.value"
|
|
@change="
|
|
$store.commit('setValueHeader', {
|
|
index,
|
|
value: $event.target.value,
|
|
})
|
|
"
|
|
@keyup.prevent="setRouteQueryState"
|
|
/>
|
|
</li>
|
|
<div>
|
|
<li>
|
|
<button
|
|
class="icon"
|
|
@click="removeRequestHeader(index)"
|
|
v-tooltip.bottom="$t('delete')"
|
|
id="header"
|
|
>
|
|
<i class="material-icons">delete</i>
|
|
</button>
|
|
</li>
|
|
</div>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<button class="icon" @click="addRequestHeader">
|
|
<i class="material-icons">add</i>
|
|
<span>{{ $t('add_new') }}</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</pw-section>
|
|
</div>
|
|
<input id="tab-three" type="radio" name="options" />
|
|
<label for="tab-three">{{ $t('parameters') }}</label>
|
|
<div class="tab">
|
|
<pw-section class="pink" label="Parameters" ref="parameters">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="paramList">{{ $t('parameter_list') }}</label>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="clearContent('parameters', $event)"
|
|
v-tooltip.bottom="$t('clear')"
|
|
>
|
|
<i class="material-icons">clear_all</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<textarea
|
|
id="paramList"
|
|
readonly
|
|
v-textarea-auto-height="queryString"
|
|
v-model="queryString"
|
|
:placeholder="$t('add_one_parameter')"
|
|
rows="1"
|
|
></textarea>
|
|
</li>
|
|
</ul>
|
|
<ul v-for="(param, index) in params" :key="index">
|
|
<li>
|
|
<input
|
|
:placeholder="$t('parameter_count', { count: index + 1 })"
|
|
:name="'param' + index"
|
|
:value="param.key"
|
|
@change="
|
|
$store.commit('setKeyParams', {
|
|
index,
|
|
value: $event.target.value,
|
|
})
|
|
"
|
|
autofocus
|
|
/>
|
|
</li>
|
|
<li>
|
|
<input
|
|
:placeholder="$t('value_count', { count: index + 1 })"
|
|
:name="'value' + index"
|
|
:value="param.value"
|
|
@change="
|
|
$store.commit('setValueParams', {
|
|
index,
|
|
value: $event.target.value,
|
|
})
|
|
"
|
|
/>
|
|
</li>
|
|
<div>
|
|
<li>
|
|
<button
|
|
class="icon"
|
|
@click="removeRequestParam(index)"
|
|
v-tooltip.bottom="$t('delete')"
|
|
id="param"
|
|
>
|
|
<i class="material-icons">delete</i>
|
|
</button>
|
|
</li>
|
|
</div>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<button class="icon" @click="addRequestParam">
|
|
<i class="material-icons">add</i>
|
|
<span>{{ $t('add_new') }}</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</pw-section>
|
|
</div>
|
|
<!-- <div class="flex-wrap">
|
|
<span></span>
|
|
<button
|
|
class="icon hide-on-small-screen"
|
|
@click="activeSidebar = !activeSidebar"
|
|
v-tooltip="{
|
|
content: activeSidebar ? 'Hide Sidebar' : 'Show Sidebar'
|
|
}"
|
|
>
|
|
<i class="material-icons">
|
|
{{ activeSidebar ? "last_page" : "first_page" }}
|
|
</i>
|
|
</button>
|
|
</div> -->
|
|
</section>
|
|
|
|
<pw-section class="purple" id="response" :label="$t('response')" ref="response">
|
|
<ul>
|
|
<li>
|
|
<label for="status">{{ $t('status') }}</label>
|
|
<input
|
|
:class="statusCategory ? statusCategory.className : ''"
|
|
:value="response.status || $t('waiting_send_req')"
|
|
ref="status"
|
|
id="status"
|
|
name="status"
|
|
readonly
|
|
type="text"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
<ul v-for="(value, key) in response.headers" :key="key">
|
|
<li>
|
|
<label :for="key">{{ key }}</label>
|
|
<input :id="key" :value="value" :name="key" readonly />
|
|
</li>
|
|
</ul>
|
|
<ul v-if="response.body">
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="body">{{ $t('response') }}</label>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="ToggleExpandResponse"
|
|
ref="ToggleExpandResponse"
|
|
v-if="response.body"
|
|
v-tooltip="{
|
|
content: !expandResponse ? $t('expand_response') : $t('collapse_response'),
|
|
}"
|
|
>
|
|
<i class="material-icons">
|
|
{{ !expandResponse ? 'unfold_more' : 'unfold_less' }}
|
|
</i>
|
|
</button>
|
|
<button
|
|
class="icon"
|
|
@click="downloadResponse"
|
|
ref="downloadResponse"
|
|
v-if="response.body"
|
|
v-tooltip="$t('download_file')"
|
|
>
|
|
<i class="material-icons">get_app</i>
|
|
</button>
|
|
<button
|
|
class="icon"
|
|
@click="copyResponse"
|
|
ref="copyResponse"
|
|
v-if="response.body"
|
|
v-tooltip="$t('copy_response')"
|
|
>
|
|
<i class="material-icons">file_copy</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="response-details-wrapper">
|
|
<Editor
|
|
:value="responseBodyText"
|
|
:lang="responseBodyType"
|
|
:options="{
|
|
maxLines: responseBodyMaxLines,
|
|
minLines: '16',
|
|
fontSize: '16px',
|
|
autoScrollEditorIntoView: true,
|
|
readOnly: true,
|
|
showPrintMargin: false,
|
|
useWorker: false,
|
|
}"
|
|
/>
|
|
<iframe
|
|
:class="{ hidden: !previewEnabled }"
|
|
class="covers-response"
|
|
ref="previewFrame"
|
|
src="about:blank"
|
|
></iframe>
|
|
</div>
|
|
<div class="align-right" v-if="response.body && responseType === 'text/html'">
|
|
<button class="icon" @click.prevent="togglePreview">
|
|
<i class="material-icons">
|
|
{{ !previewEnabled ? 'visibility' : 'visibility_off' }}
|
|
</i>
|
|
<span>
|
|
{{ previewEnabled ? $t('hide_preview') : $t('preview_html') }}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</pw-section>
|
|
</div>
|
|
<aside v-if="activeSidebar" class="sticky-inner inner-right">
|
|
<section>
|
|
<input id="history-tab" type="radio" name="side" checked="checked" />
|
|
<label for="history-tab">{{ $t('history') }}</label>
|
|
<div class="tab">
|
|
<history @useHistory="handleUseHistory" ref="historyComponent" />
|
|
</div>
|
|
<input id="collection-tab" type="radio" name="side" />
|
|
<label for="collection-tab">{{ $t('collections') }}</label>
|
|
<div class="tab">
|
|
<collections />
|
|
</div>
|
|
<input id="environment-tab" type="radio" name="side" />
|
|
<label for="environment-tab">{{ $t('environment') }}</label>
|
|
<div class="tab">
|
|
<environments @use-environment="useSelectedEnvironment($event)" />
|
|
</div>
|
|
<input id="sync-tab" type="radio" name="side" />
|
|
<label for="sync-tab">{{ $t('notes') }}</label>
|
|
<div class="tab">
|
|
<pw-section class="pink" :label="$t('notes')" ref="sync">
|
|
<div v-if="fb.currentUser">
|
|
<inputform />
|
|
<notes />
|
|
</div>
|
|
<div v-else>
|
|
<ul>
|
|
<li>
|
|
<label>{{ $t('login_first') }}</label>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</pw-section>
|
|
</div>
|
|
</section>
|
|
</aside>
|
|
|
|
<save-request-as
|
|
:show="showRequestModal"
|
|
@hide-model="hideRequestModal"
|
|
:editing-request="editRequest"
|
|
/>
|
|
|
|
<pw-modal v-if="showModal" @close="showModal = false">
|
|
<div slot="header">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<h3 class="title">{{ $t('import_curl') }}</h3>
|
|
<div>
|
|
<button class="icon" @click="showModal = false">
|
|
<i class="material-icons">close</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div slot="body">
|
|
<ul>
|
|
<li>
|
|
<textarea
|
|
id="import-text"
|
|
autofocus
|
|
rows="8"
|
|
:placeholder="$t('enter_curl')"
|
|
></textarea>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div slot="footer">
|
|
<div class="flex-wrap">
|
|
<span></span>
|
|
<span>
|
|
<button class="icon" @click="showModal = false">
|
|
{{ $t('cancel') }}
|
|
</button>
|
|
<button class="icon primary" @click="handleImport">
|
|
{{ $t('import') }}
|
|
</button>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</pw-modal>
|
|
|
|
<pw-modal v-if="!isHidden" @close="isHidden = true">
|
|
<div slot="header">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<h3 class="title">{{ $t('generate_code') }}</h3>
|
|
<div>
|
|
<button class="icon" @click="isHidden = true">
|
|
<i class="material-icons">close</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div slot="body">
|
|
<ul>
|
|
<li>
|
|
<label for="requestType">{{ $t('request_type') }}</label>
|
|
<span class="select-wrapper">
|
|
<select id="requestType" v-model="requestType">
|
|
<option>JavaScript XHR</option>
|
|
<option>Fetch</option>
|
|
<option>cURL</option>
|
|
</select>
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="generatedCode">{{ $t('generated_code') }}</label>
|
|
<div>
|
|
<button
|
|
class="icon"
|
|
@click="copyRequestCode"
|
|
id="copyRequestCode"
|
|
ref="copyRequestCode"
|
|
v-tooltip="$t('copy_code')"
|
|
>
|
|
<i class="material-icons">file_copy</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<textarea
|
|
id="generatedCode"
|
|
ref="generatedCode"
|
|
name="generatedCode"
|
|
rows="8"
|
|
v-model="requestCode"
|
|
></textarea>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div slot="footer"></div>
|
|
</pw-modal>
|
|
|
|
<pw-modal v-if="showTokenList" @close="showTokenList = false">
|
|
<div slot="header">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<h3 class="title">{{ $t('manage_token') }}</h3>
|
|
<div>
|
|
<button class="icon" @click="showTokenList = false">
|
|
<i class="material-icons">close</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div slot="body">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="token-list">{{ $t('token_list') }}</label>
|
|
<div v-if="tokens.length != 0">
|
|
<button
|
|
class="icon"
|
|
@click="clearContent('tokens', $event)"
|
|
v-tooltip.bottom="$t('clear')"
|
|
>
|
|
<i class="material-icons">clear_all</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<ul id="token-list" v-for="(token, index) in tokens" :key="index">
|
|
<li>
|
|
<input
|
|
:placeholder="'name ' + (index + 1)"
|
|
:value="token.name"
|
|
@change="
|
|
$store.commit('setOAuthTokenName', {
|
|
index,
|
|
value: $event.target.value,
|
|
})
|
|
"
|
|
/>
|
|
</li>
|
|
<li>
|
|
<input :value="token.value" readonly />
|
|
</li>
|
|
<div class="flex-wrap">
|
|
<li>
|
|
<button
|
|
class="icon"
|
|
@click="useOAuthToken(token.value)"
|
|
v-tooltip.bottom="$t('use_token')"
|
|
>
|
|
<i class="material-icons">input</i>
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button
|
|
class="icon"
|
|
@click="removeOAuthToken(index)"
|
|
v-tooltip.bottom="$t('delete')"
|
|
>
|
|
<i class="material-icons">delete</i>
|
|
</button>
|
|
</li>
|
|
</div>
|
|
</ul>
|
|
<p v-if="tokens.length === 0" class="info">
|
|
{{ $t('empty') }}
|
|
</p>
|
|
</div>
|
|
<div slot="footer"></div>
|
|
</pw-modal>
|
|
|
|
<pw-modal v-if="showTokenRequestList" @close="showTokenRequestList = false">
|
|
<div slot="header">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<h3 class="title">{{ $t('manage_token_req') }}</h3>
|
|
<div>
|
|
<button class="icon" @click="showTokenRequestList = false">
|
|
<i class="material-icons">close</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div slot="body">
|
|
<ul>
|
|
<li>
|
|
<div class="flex-wrap">
|
|
<label for="token-req-list">{{ $t('token_req_list') }}</label>
|
|
<div>
|
|
<button
|
|
:disabled="this.tokenReqs.length === 0"
|
|
class="icon"
|
|
@click="showTokenRequestList = false"
|
|
v-tooltip.bottom="$t('use_token_req')"
|
|
>
|
|
<i class="material-icons">input</i>
|
|
</button>
|
|
<button
|
|
:disabled="this.tokenReqs.length === 0"
|
|
class="icon"
|
|
@click="removeOAuthTokenReq"
|
|
v-tooltip.bottom="$t('delete')"
|
|
>
|
|
<i class="material-icons">delete</i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<span class="select-wrapper">
|
|
<select
|
|
id="token-req-list"
|
|
v-model="tokenReqSelect"
|
|
:disabled="this.tokenReqs.length === 0"
|
|
@change="tokenReqChange($event)"
|
|
>
|
|
<option v-for="(req, index) in tokenReqs" :key="index" :value="req.name">
|
|
{{ req.name }}
|
|
</option>
|
|
</select>
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<label for="token-req-name">{{ $t('token_req_name') }}</label>
|
|
<input v-model="tokenReqName" />
|
|
</li>
|
|
</ul>
|
|
<ul>
|
|
<li>
|
|
<label for="token-req-details">
|
|
{{ $t('token_req_details') }}
|
|
</label>
|
|
<textarea
|
|
id="token-req-details"
|
|
readonly
|
|
rows="7"
|
|
v-model="tokenReqDetails"
|
|
></textarea>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div slot="footer">
|
|
<div class="flex-wrap">
|
|
<span></span>
|
|
<span>
|
|
<button class="icon primary" @click="addOAuthTokenReq">
|
|
{{ $t('save_token_req') }}
|
|
</button>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</pw-modal>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import section from '../components/section'
|
|
import url from 'url'
|
|
import querystring from 'querystring'
|
|
import { commonHeaders } from '../functions/headers'
|
|
import textareaAutoHeight from '../directives/textareaAutoHeight'
|
|
import parseCurlCommand from '../assets/js/curlparser.js'
|
|
import getEnvironmentVariablesFromScript from '../functions/preRequest'
|
|
import runTestScriptWithVariables from '../functions/postwomanTesting'
|
|
import parseTemplateString from '../functions/templating'
|
|
import AceEditor from '../components/ace-editor'
|
|
import { tokenRequest, oauthRedirect } from '../assets/js/oauth'
|
|
import { sendNetworkRequest } from '../functions/network'
|
|
import { fb } from '../functions/fb'
|
|
import { getEditorLangForMimeType } from '~/functions/editorutils'
|
|
|
|
const statusCategories = [
|
|
{
|
|
name: 'informational',
|
|
statusCodeRegex: new RegExp(/[1][0-9]+/),
|
|
className: 'info-response',
|
|
},
|
|
{
|
|
name: 'successful',
|
|
statusCodeRegex: new RegExp(/[2][0-9]+/),
|
|
className: 'success-response',
|
|
},
|
|
{
|
|
name: 'redirection',
|
|
statusCodeRegex: new RegExp(/[3][0-9]+/),
|
|
className: 'redir-response',
|
|
},
|
|
{
|
|
name: 'client error',
|
|
statusCodeRegex: new RegExp(/[4][0-9]+/),
|
|
className: 'cl-error-response',
|
|
},
|
|
{
|
|
name: 'server error',
|
|
statusCodeRegex: new RegExp(/[5][0-9]+/),
|
|
className: 'sv-error-response',
|
|
},
|
|
{
|
|
// this object is a catch-all for when no other objects match and should always be last
|
|
name: 'unknown',
|
|
statusCodeRegex: new RegExp(/.*/),
|
|
className: 'missing-data-response',
|
|
},
|
|
]
|
|
const parseHeaders = xhr => {
|
|
const headers = xhr
|
|
.getAllResponseHeaders()
|
|
.trim()
|
|
.split(/[\r\n]+/)
|
|
const headerMap = {}
|
|
headers.forEach(line => {
|
|
const parts = line.split(': ')
|
|
const header = parts.shift().toLowerCase()
|
|
const value = parts.join(': ')
|
|
headerMap[header] = value
|
|
})
|
|
return headerMap
|
|
}
|
|
export const findStatusGroup = responseStatus =>
|
|
statusCategories.find(status => status.statusCodeRegex.test(responseStatus))
|
|
|
|
export default {
|
|
directives: {
|
|
textareaAutoHeight,
|
|
},
|
|
|
|
components: {
|
|
'pw-section': section,
|
|
'pw-toggle': () => import('../components/toggle'),
|
|
'pw-modal': () => import('../components/modal'),
|
|
history: () => import('../components/history'),
|
|
autocomplete: () => import('../components/autocomplete'),
|
|
collections: () => import('../components/collections'),
|
|
saveRequestAs: () => import('../components/collections/saveRequestAs'),
|
|
Editor: AceEditor,
|
|
inputform: () => import('../components/firebase/inputform'),
|
|
notes: () => import('../components/firebase/feeds'),
|
|
environments: () => import('../components/environments'),
|
|
},
|
|
data() {
|
|
return {
|
|
showModal: false,
|
|
showPreRequestScript: false,
|
|
testsEnabled: false,
|
|
testScript: "// pw.expect('variable').toBe('value');",
|
|
preRequestScript: "// pw.env.set('variable', 'value');",
|
|
testReports: null,
|
|
copyButton: '<i class="material-icons">file_copy</i>',
|
|
downloadButton: '<i class="material-icons">get_app</i>',
|
|
doneButton: '<i class="material-icons">done</i>',
|
|
isHidden: true,
|
|
response: {
|
|
status: '',
|
|
headers: '',
|
|
body: '',
|
|
},
|
|
previewEnabled: false,
|
|
paramsWatchEnabled: true,
|
|
expandResponse: false,
|
|
showTokenList: false,
|
|
showTokenRequest: false,
|
|
showTokenRequestList: false,
|
|
|
|
/**
|
|
* These are content types that can be automatically
|
|
* serialized by postwoman.
|
|
*/
|
|
knownContentTypes: ['application/json', 'application/x-www-form-urlencoded'],
|
|
|
|
/**
|
|
* These are a list of Content Types known to Postwoman.
|
|
*/
|
|
validContentTypes: [
|
|
'application/json',
|
|
'application/hal+json',
|
|
'application/xml',
|
|
'application/x-www-form-urlencoded',
|
|
'text/html',
|
|
'text/plain',
|
|
],
|
|
|
|
commonHeaders,
|
|
|
|
showRequestModal: false,
|
|
editRequest: {},
|
|
urlExcludes: {},
|
|
responseBodyText: '',
|
|
responseBodyType: 'text',
|
|
responseBodyMaxLines: 16,
|
|
activeSidebar: true,
|
|
fb,
|
|
customMethod: false,
|
|
files: [],
|
|
filenames: '',
|
|
navigatorShare: navigator.share,
|
|
}
|
|
},
|
|
watch: {
|
|
urlExcludes: {
|
|
deep: true,
|
|
handler() {
|
|
this.$store.commit('postwoman/applySetting', [
|
|
'URL_EXCLUDES',
|
|
Object.assign({}, this.urlExcludes),
|
|
])
|
|
},
|
|
},
|
|
contentType(val) {
|
|
this.rawInput = !this.knownContentTypes.includes(val)
|
|
},
|
|
rawInput(status) {
|
|
if (status && this.rawParams === '') {
|
|
this.rawParams = '{}'
|
|
} else {
|
|
this.setRouteQueryState()
|
|
}
|
|
},
|
|
'response.body': function(val) {
|
|
if (
|
|
this.response.body === this.$t('waiting_send_req') ||
|
|
this.response.body === this.$t('loading')
|
|
) {
|
|
this.responseBodyText = this.response.body
|
|
this.responseBodyType = 'text'
|
|
} else {
|
|
if (
|
|
this.responseType === 'application/json' ||
|
|
this.responseType === 'application/hal+json'
|
|
) {
|
|
this.responseBodyText = JSON.stringify(this.response.body, null, 2)
|
|
this.responseBodyType = 'json'
|
|
} else if (this.responseType === 'text/html') {
|
|
this.responseBodyText = this.response.body
|
|
this.responseBodyType = 'html'
|
|
} else {
|
|
this.responseBodyText = this.response.body
|
|
this.responseBodyType = 'text'
|
|
}
|
|
}
|
|
},
|
|
params: {
|
|
handler: function(newValue) {
|
|
if (!this.paramsWatchEnabled) {
|
|
this.paramsWatchEnabled = true
|
|
return
|
|
}
|
|
let path = this.path
|
|
let queryString = newValue
|
|
.filter(({ key }) => !!key)
|
|
.map(({ key, value }) => `${key}=${value}`)
|
|
.join('&')
|
|
queryString = queryString === '' ? '' : `?${queryString}`
|
|
if (path.includes('?')) {
|
|
path = path.slice(0, path.indexOf('?')) + queryString
|
|
} else {
|
|
path = path + queryString
|
|
}
|
|
|
|
this.path = path
|
|
},
|
|
deep: true,
|
|
},
|
|
selectedRequest(newValue, oldValue) {
|
|
// @TODO: Convert all variables to single request variable
|
|
if (!newValue) return
|
|
this.url = newValue.url
|
|
this.path = newValue.path
|
|
this.method = newValue.method
|
|
this.auth = newValue.auth
|
|
this.httpUser = newValue.httpUser
|
|
this.httpPassword = newValue.httpPassword
|
|
this.passwordFieldType = newValue.passwordFieldType
|
|
this.bearerToken = newValue.bearerToken
|
|
this.headers = newValue.headers
|
|
this.params = newValue.params
|
|
this.bodyParams = newValue.bodyParams
|
|
this.rawParams = newValue.rawParams
|
|
this.rawInput = newValue.rawInput
|
|
this.contentType = newValue.contentType
|
|
this.requestType = newValue.requestType
|
|
},
|
|
editingRequest(newValue) {
|
|
this.editRequest = newValue
|
|
this.showRequestModal = true
|
|
},
|
|
method() {
|
|
// this.$store.commit('setState', { 'value': ["POST", "PUT", "PATCH"].includes(this.method) ? 'application/json' : '', 'attribute': 'contentType' })
|
|
this.contentType = ['POST', 'PUT', 'PATCH'].includes(this.method) ? 'application/json' : ''
|
|
},
|
|
},
|
|
computed: {
|
|
url: {
|
|
get() {
|
|
return this.$store.state.request.url
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'url' })
|
|
},
|
|
},
|
|
method: {
|
|
get() {
|
|
return this.$store.state.request.method
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'method' })
|
|
},
|
|
},
|
|
path: {
|
|
get() {
|
|
return this.$store.state.request.path
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'path' })
|
|
},
|
|
},
|
|
label: {
|
|
get() {
|
|
return this.$store.state.request.label
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'label' })
|
|
},
|
|
},
|
|
auth: {
|
|
get() {
|
|
return this.$store.state.request.auth
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'auth' })
|
|
},
|
|
},
|
|
httpUser: {
|
|
get() {
|
|
return this.$store.state.request.httpUser
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'httpUser' })
|
|
},
|
|
},
|
|
httpPassword: {
|
|
get() {
|
|
return this.$store.state.request.httpPassword
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'httpPassword' })
|
|
},
|
|
},
|
|
bearerToken: {
|
|
get() {
|
|
return this.$store.state.request.bearerToken
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'bearerToken' })
|
|
},
|
|
},
|
|
tokens: {
|
|
get() {
|
|
return this.$store.state.oauth2.tokens
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', { value, attribute: 'tokens' })
|
|
},
|
|
},
|
|
tokenReqs: {
|
|
get() {
|
|
return this.$store.state.oauth2.tokenReqs
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', { value, attribute: 'tokenReqs' })
|
|
},
|
|
},
|
|
tokenReqSelect: {
|
|
get() {
|
|
return this.$store.state.oauth2.tokenReqSelect
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', { value, attribute: 'tokenReqSelect' })
|
|
},
|
|
},
|
|
tokenReqName: {
|
|
get() {
|
|
return this.$store.state.oauth2.tokenReqName
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', { value, attribute: 'tokenReqName' })
|
|
},
|
|
},
|
|
accessTokenName: {
|
|
get() {
|
|
return this.$store.state.oauth2.accessTokenName
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', {
|
|
value,
|
|
attribute: 'accessTokenName',
|
|
})
|
|
},
|
|
},
|
|
oidcDiscoveryUrl: {
|
|
get() {
|
|
return this.$store.state.oauth2.oidcDiscoveryUrl
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', {
|
|
value,
|
|
attribute: 'oidcDiscoveryUrl',
|
|
})
|
|
},
|
|
},
|
|
authUrl: {
|
|
get() {
|
|
return this.$store.state.oauth2.authUrl
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', { value, attribute: 'authUrl' })
|
|
},
|
|
},
|
|
accessTokenUrl: {
|
|
get() {
|
|
return this.$store.state.oauth2.accessTokenUrl
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', { value, attribute: 'accessTokenUrl' })
|
|
},
|
|
},
|
|
clientId: {
|
|
get() {
|
|
return this.$store.state.oauth2.clientId
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', { value, attribute: 'clientId' })
|
|
},
|
|
},
|
|
scope: {
|
|
get() {
|
|
return this.$store.state.oauth2.scope
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', { value, attribute: 'scope' })
|
|
},
|
|
},
|
|
state: {
|
|
get() {
|
|
return this.$store.state.oauth2.state
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setOAuth2', { value, attribute: 'state' })
|
|
},
|
|
},
|
|
headers: {
|
|
get() {
|
|
return this.$store.state.request.headers
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'headers' })
|
|
},
|
|
},
|
|
params: {
|
|
get() {
|
|
return this.$store.state.request.params
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'params' })
|
|
},
|
|
},
|
|
bodyParams: {
|
|
get() {
|
|
return this.$store.state.request.bodyParams
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'bodyParams' })
|
|
},
|
|
},
|
|
rawParams: {
|
|
get() {
|
|
return this.$store.state.request.rawParams
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'rawParams' })
|
|
},
|
|
},
|
|
rawInput: {
|
|
get() {
|
|
return this.$store.state.request.rawInput
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'rawInput' })
|
|
},
|
|
},
|
|
rawInputEditorLang() {
|
|
return getEditorLangForMimeType(this.contentType)
|
|
},
|
|
requestType: {
|
|
get() {
|
|
return this.$store.state.request.requestType
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'requestType' })
|
|
},
|
|
},
|
|
contentType: {
|
|
get() {
|
|
return this.$store.state.request.contentType
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', { value, attribute: 'contentType' })
|
|
},
|
|
},
|
|
passwordFieldType: {
|
|
get() {
|
|
return this.$store.state.request.passwordFieldType
|
|
},
|
|
set(value) {
|
|
this.$store.commit('setState', {
|
|
value,
|
|
attribute: 'passwordFieldType',
|
|
})
|
|
},
|
|
},
|
|
|
|
selectedRequest() {
|
|
return this.$store.state.postwoman.selectedRequest
|
|
},
|
|
editingRequest() {
|
|
return this.$store.state.postwoman.editingRequest
|
|
},
|
|
requestName() {
|
|
return this.label
|
|
},
|
|
statusCategory() {
|
|
return findStatusGroup(this.response.status)
|
|
},
|
|
isValidURL() {
|
|
if (this.showPreRequestScript) {
|
|
// we cannot determine if a URL is valid because the full string is not known ahead of time
|
|
return true
|
|
}
|
|
const protocol = '^(https?:\\/\\/)?'
|
|
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])$`
|
|
)
|
|
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/])$`
|
|
)
|
|
return validIP.test(this.url) || validHostname.test(this.url)
|
|
},
|
|
hasRequestBody() {
|
|
return ['POST', 'PUT', 'PATCH'].includes(this.method)
|
|
},
|
|
pathName() {
|
|
return this.path.match(/^([^?]*)\??/)[1]
|
|
},
|
|
rawRequestBody() {
|
|
const { bodyParams, contentType } = this
|
|
if (contentType === 'application/json') {
|
|
try {
|
|
const obj = JSON.parse(
|
|
`{${bodyParams
|
|
.filter(({ key }) => !!key)
|
|
.map(
|
|
({ key, value }) => `
|
|
"${key}": "${value}"
|
|
`
|
|
)
|
|
.join()}}`
|
|
)
|
|
return JSON.stringify(obj, null, 2)
|
|
} catch (ex) {
|
|
return 'invalid'
|
|
}
|
|
} else {
|
|
return bodyParams
|
|
.filter(({ key }) => !!key)
|
|
.map(({ key, value }) => `${key}=${encodeURIComponent(value)}`)
|
|
.join('&')
|
|
}
|
|
},
|
|
headerString() {
|
|
const result = this.headers
|
|
.filter(({ key }) => !!key)
|
|
.map(({ key, value }) => `${key}: ${value}`)
|
|
.join(',\n')
|
|
return result === '' ? '' : `${result}`
|
|
},
|
|
queryString() {
|
|
const result = this.params
|
|
.filter(({ key }) => !!key)
|
|
.map(({ key, value }) => `${key}=${encodeURIComponent(value)}`)
|
|
.join('&')
|
|
return result === '' ? '' : `?${result}`
|
|
},
|
|
responseType() {
|
|
return (this.response.headers['content-type'] || '').split(';')[0].toLowerCase()
|
|
},
|
|
requestCode() {
|
|
if (this.requestType === 'JavaScript XHR') {
|
|
const requestString = []
|
|
requestString.push('const xhr = new XMLHttpRequest()')
|
|
const user = this.auth === 'Basic Auth' ? `'${this.httpUser}'` : null
|
|
const password = this.auth === 'Basic Auth' ? `'${this.httpPassword}'` : null
|
|
requestString.push(
|
|
`xhr.open('${this.method}', '${this.url}${this.pathName}${this.queryString}', true, ${user}, ${password})`
|
|
)
|
|
if (this.auth === 'Bearer Token' || this.auth === 'OAuth 2.0') {
|
|
requestString.push(`xhr.setRequestHeader('Authorization', 'Bearer ${this.bearerToken}')`)
|
|
}
|
|
if (this.headers) {
|
|
this.headers.forEach(({ key, value }) => {
|
|
requestString.push(`xhr.setRequestHeader('${key}', '${value}')`)
|
|
})
|
|
}
|
|
if (['POST', 'PUT', 'PATCH'].includes(this.method)) {
|
|
const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody
|
|
requestString.push(`xhr.setRequestHeader('Content-Length', ${requestBody.length})`)
|
|
requestString.push(
|
|
`xhr.setRequestHeader('Content-Type', '${this.contentType}; charset=utf-8')`
|
|
)
|
|
requestString.push(`xhr.send(${requestBody})`)
|
|
} else {
|
|
requestString.push('xhr.send()')
|
|
}
|
|
return requestString.join('\n')
|
|
} else if (this.requestType === 'Fetch') {
|
|
const requestString = []
|
|
let headers = []
|
|
requestString.push(`fetch("${this.url}${this.pathName}${this.queryString}", {\n`)
|
|
requestString.push(` method: "${this.method}",\n`)
|
|
if (this.auth === 'Basic Auth') {
|
|
const basic = `${this.httpUser}:${this.httpPassword}`
|
|
headers.push(
|
|
` "Authorization": "Basic ${window.btoa(unescape(encodeURIComponent(basic)))}",\n`
|
|
)
|
|
} else if (this.auth === 'Bearer Token' || this.auth === 'OAuth 2.0') {
|
|
headers.push(` "Authorization": "Bearer ${this.bearerToken}",\n`)
|
|
}
|
|
if (['POST', 'PUT', 'PATCH'].includes(this.method)) {
|
|
const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody
|
|
requestString.push(` body: ${requestBody},\n`)
|
|
headers.push(` "Content-Length": ${requestBody.length},\n`)
|
|
headers.push(` "Content-Type": "${this.contentType}; charset=utf-8",\n`)
|
|
}
|
|
if (this.headers) {
|
|
this.headers.forEach(({ key, value }) => {
|
|
headers.push(` "${key}": "${value}",\n`)
|
|
})
|
|
}
|
|
headers = headers.join('').slice(0, -2)
|
|
requestString.push(` headers: {\n${headers}\n },\n`)
|
|
requestString.push(' credentials: "same-origin"\n')
|
|
requestString.push('}).then(function(response) {\n')
|
|
requestString.push(' response.status\n')
|
|
requestString.push(' response.statusText\n')
|
|
requestString.push(' response.headers\n')
|
|
requestString.push(' response.url\n\n')
|
|
requestString.push(' return response.text()\n')
|
|
requestString.push('}).catch(function(error) {\n')
|
|
requestString.push(' error.message\n')
|
|
requestString.push('})')
|
|
return requestString.join('')
|
|
} else if (this.requestType === 'cURL') {
|
|
const requestString = []
|
|
requestString.push(`curl -X ${this.method} \n`)
|
|
requestString.push(` '${this.url}${this.pathName}${this.queryString}' \n`)
|
|
if (this.auth === 'Basic Auth') {
|
|
const basic = `${this.httpUser}:${this.httpPassword}`
|
|
requestString.push(
|
|
` -H 'Authorization: Basic ${window.btoa(unescape(encodeURIComponent(basic)))}' \n`
|
|
)
|
|
} else if (this.auth === 'Bearer Token' || this.auth === 'OAuth 2.0') {
|
|
requestString.push(` -H 'Authorization: Bearer ${this.bearerToken}' \n`)
|
|
}
|
|
if (this.headers) {
|
|
this.headers.forEach(({ key, value }) => {
|
|
requestString.push(` -H '${key}: ${value}' \n`)
|
|
})
|
|
}
|
|
if (['POST', 'PUT', 'PATCH'].includes(this.method)) {
|
|
const requestBody = this.rawInput ? this.rawParams : this.rawRequestBody
|
|
requestString.push(` -H 'Content-Length: ${requestBody.length}' \n`)
|
|
requestString.push(` -H 'Content-Type: ${this.contentType}; charset=utf-8' \n`)
|
|
requestString.push(` -d '${requestBody}' \n`)
|
|
}
|
|
return requestString.join('').slice(0, -2)
|
|
}
|
|
},
|
|
tokenReqDetails() {
|
|
const details = {
|
|
oidcDiscoveryUrl: this.oidcDiscoveryUrl,
|
|
authUrl: this.authUrl,
|
|
accessTokenUrl: this.accessTokenUrl,
|
|
clientId: this.clientId,
|
|
scope: this.scope,
|
|
}
|
|
return JSON.stringify(details, null, 2)
|
|
},
|
|
},
|
|
methods: {
|
|
useSelectedEnvironment(environment) {
|
|
let preRequestScriptString = ''
|
|
for (let variable of environment.variables) {
|
|
preRequestScriptString =
|
|
preRequestScriptString + `pw.env.set('${variable.key}', '${variable.value}');\n`
|
|
}
|
|
this.preRequestScript = preRequestScriptString
|
|
this.showPreRequestScript = true
|
|
},
|
|
checkCollections() {
|
|
const checkCollectionAvailability =
|
|
this.$store.state.postwoman.collections &&
|
|
this.$store.state.postwoman.collections.length > 0
|
|
return checkCollectionAvailability
|
|
},
|
|
scrollInto(view) {
|
|
this.$refs[view].$el.scrollIntoView({
|
|
behavior: 'smooth',
|
|
})
|
|
},
|
|
handleUseHistory({ label, method, url, path, usesScripts, preRequestScript }) {
|
|
this.label = label
|
|
this.method = method
|
|
this.url = url
|
|
this.path = path
|
|
this.showPreRequestScript = usesScripts
|
|
this.preRequestScript = preRequestScript
|
|
this.$store.state.postwoman.settings.SCROLL_INTO_ENABLED && this.scrollInto('request')
|
|
},
|
|
getVariablesFromPreRequestScript() {
|
|
if (!this.preRequestScript) {
|
|
return {}
|
|
}
|
|
return getEnvironmentVariablesFromScript(this.preRequestScript)
|
|
},
|
|
async makeRequest(auth, headers, requestBody, preRequestScript) {
|
|
const requestOptions = {
|
|
method: this.method,
|
|
url: this.url + this.pathName + this.queryString,
|
|
auth,
|
|
headers,
|
|
data: requestBody,
|
|
credentials: true,
|
|
}
|
|
if (preRequestScript) {
|
|
const environmentVariables = getEnvironmentVariablesFromScript(preRequestScript)
|
|
requestOptions.url = parseTemplateString(requestOptions.url, environmentVariables)
|
|
requestOptions.data = parseTemplateString(requestOptions.data, environmentVariables)
|
|
for (let k in requestOptions.headers) {
|
|
const kParsed = parseTemplateString(k, environmentVariables)
|
|
const valParsed = parseTemplateString(requestOptions.headers[k], environmentVariables)
|
|
delete requestOptions.headers[k]
|
|
requestOptions.headers[kParsed] = valParsed
|
|
}
|
|
}
|
|
if (typeof requestOptions.data === 'string') {
|
|
requestOptions.data = parseTemplateString(requestOptions.data)
|
|
}
|
|
|
|
return await sendNetworkRequest(requestOptions, this.$store)
|
|
},
|
|
async sendRequest() {
|
|
this.$toast.clear()
|
|
this.$store.state.postwoman.settings.SCROLL_INTO_ENABLED && this.scrollInto('response')
|
|
|
|
if (!this.isValidURL) {
|
|
this.$toast.error(this.$t('url_invalid_format'), {
|
|
icon: 'error',
|
|
})
|
|
return
|
|
}
|
|
|
|
// Start showing the loading bar as soon as possible.
|
|
// The nuxt axios module will hide it when the request is made.
|
|
this.$nuxt.$loading.start()
|
|
|
|
if (this.$refs.response.$el.classList.contains('hidden')) {
|
|
this.$refs.response.$el.classList.toggle('hidden')
|
|
}
|
|
this.previewEnabled = false
|
|
this.response.status = this.$t('fetching')
|
|
this.response.body = this.$t('loading')
|
|
|
|
const auth =
|
|
this.auth === 'Basic Auth'
|
|
? {
|
|
username: this.httpUser,
|
|
password: this.httpPassword,
|
|
}
|
|
: null
|
|
|
|
let headers = {}
|
|
let headersObject = {}
|
|
|
|
Object.keys(headers).forEach(id => {
|
|
headersObject[headers[id].key] = headers[id].value
|
|
})
|
|
headers = headersObject
|
|
|
|
// If the request has a body, we want to ensure Content-Length and
|
|
// Content-Type are sent.
|
|
let requestBody
|
|
if (this.hasRequestBody) {
|
|
requestBody = this.rawInput ? this.rawParams : this.rawRequestBody
|
|
|
|
Object.assign(headers, {
|
|
//'Content-Length': requestBody.length,
|
|
'Content-Type': `${this.contentType}; charset=utf-8`,
|
|
})
|
|
}
|
|
|
|
requestBody = requestBody ? requestBody.toString() : null
|
|
|
|
if (this.files.length !== 0) {
|
|
const formData = new FormData()
|
|
for (let i = 0; i < this.files.length; i++) {
|
|
let file = this.files[i]
|
|
formData.append(`files[${i}]`, file)
|
|
}
|
|
formData.append('data', requestBody)
|
|
requestBody = formData
|
|
}
|
|
|
|
// If the request uses a token for auth, we want to make sure it's sent here.
|
|
if (this.auth === 'Bearer Token' || this.auth === 'OAuth 2.0')
|
|
headers['Authorization'] = `Bearer ${this.bearerToken}`
|
|
|
|
headers = Object.assign(
|
|
// Clone the app headers object first, we don't want to
|
|
// mutate it with the request headers added by default.
|
|
Object.assign({}, this.headers)
|
|
|
|
// We make our temporary headers object the source so
|
|
// that you can override the added headers if you
|
|
// specify them.
|
|
// headers
|
|
)
|
|
|
|
Object.keys(headers).forEach(id => {
|
|
headersObject[headers[id].key] = headers[id].value
|
|
})
|
|
headers = headersObject
|
|
|
|
try {
|
|
const startTime = Date.now()
|
|
|
|
const payload = await this.makeRequest(
|
|
auth,
|
|
headers,
|
|
requestBody,
|
|
this.showPreRequestScript && this.preRequestScript
|
|
)
|
|
|
|
const duration = Date.now() - startTime
|
|
this.$toast.info(this.$t('finished_in', { duration }), {
|
|
icon: 'done',
|
|
})
|
|
;(() => {
|
|
const status = (this.response.status = payload.status)
|
|
const headers = (this.response.headers = payload.headers)
|
|
|
|
// We don't need to bother parsing JSON, axios already handles it for us!
|
|
const body = (this.response.body = payload.data)
|
|
|
|
const date = new Date().toLocaleDateString()
|
|
const time = new Date().toLocaleTimeString()
|
|
|
|
// Addition of an entry to the history component.
|
|
const entry = {
|
|
label: this.requestName,
|
|
status,
|
|
date,
|
|
time,
|
|
method: this.method,
|
|
url: this.url,
|
|
path: this.path,
|
|
usesScripts: Boolean(this.preRequestScript),
|
|
preRequestScript: this.preRequestScript,
|
|
duration,
|
|
star: false,
|
|
}
|
|
this.$refs.historyComponent.addEntry(entry)
|
|
if (fb.currentUser !== null) {
|
|
if (fb.currentSettings[2].value) {
|
|
fb.writeHistory(entry)
|
|
}
|
|
}
|
|
})()
|
|
|
|
// tests
|
|
const syntheticResponse = {
|
|
status: this.response.status,
|
|
body: this.response.body,
|
|
headers: this.response.headers,
|
|
}
|
|
const { testResults } = runTestScriptWithVariables(this.testScript, {
|
|
response: syntheticResponse,
|
|
})
|
|
this.testReports = testResults
|
|
} catch (error) {
|
|
console.error(error)
|
|
if (error.response) {
|
|
this.response.headers = error.response.headers
|
|
this.response.status = error.response.status
|
|
this.response.body = error.response.data
|
|
|
|
// Addition of an entry to the history component.
|
|
const entry = {
|
|
label: this.requestName,
|
|
status: this.response.status,
|
|
date: new Date().toLocaleDateString(),
|
|
time: new Date().toLocaleTimeString(),
|
|
method: this.method,
|
|
url: this.url,
|
|
path: this.path,
|
|
usesScripts: Boolean(this.preRequestScript),
|
|
preRequestScript: this.preRequestScript,
|
|
}
|
|
this.$refs.historyComponent.addEntry(entry)
|
|
if (fb.currentUser !== null) {
|
|
if (fb.currentSettings[2].value) {
|
|
fb.writeHistory(entry)
|
|
}
|
|
}
|
|
return
|
|
} else {
|
|
this.response.status = error.message
|
|
this.response.body = `${error}. ${this.$t('check_console_details')}`
|
|
this.$toast.error(`${error} ${this.$t('f12_details')}`, {
|
|
icon: 'error',
|
|
})
|
|
if (!this.$store.state.postwoman.settings.PROXY_ENABLED) {
|
|
this.$toast.info(this.$t('enable_proxy'), {
|
|
icon: 'help',
|
|
duration: 8000,
|
|
action: {
|
|
text: this.$t('yes'),
|
|
onClick: (e, toastObject) => {
|
|
this.$router.push({ path: '/settings' })
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
},
|
|
getQueryStringFromPath() {
|
|
let queryString
|
|
const pathParsed = url.parse(this.path)
|
|
return (queryString = pathParsed.query ? pathParsed.query : '')
|
|
},
|
|
queryStringToArray(queryString) {
|
|
const queryParsed = querystring.parse(queryString)
|
|
return Object.keys(queryParsed).map(key => ({
|
|
key,
|
|
value: queryParsed[key],
|
|
}))
|
|
},
|
|
pathInputHandler() {
|
|
const queryString = this.getQueryStringFromPath()
|
|
const params = this.queryStringToArray(queryString)
|
|
this.paramsWatchEnabled = false
|
|
this.params = params
|
|
},
|
|
addRequestHeader() {
|
|
this.$store.commit('addHeaders', {
|
|
key: '',
|
|
value: '',
|
|
})
|
|
return false
|
|
},
|
|
removeRequestHeader(index) {
|
|
// .slice() gives us an entirely new array rather than giving us just the reference
|
|
const oldHeaders = this.headers.slice()
|
|
|
|
this.$store.commit('removeHeaders', index)
|
|
this.$toast.error(this.$t('deleted'), {
|
|
icon: 'delete',
|
|
action: {
|
|
text: this.$t('undo'),
|
|
onClick: (e, toastObject) => {
|
|
this.headers = oldHeaders
|
|
toastObject.remove()
|
|
},
|
|
},
|
|
})
|
|
},
|
|
addRequestParam() {
|
|
this.$store.commit('addParams', { key: '', value: '' })
|
|
return false
|
|
},
|
|
removeRequestParam(index) {
|
|
// .slice() gives us an entirely new array rather than giving us just the reference
|
|
const oldParams = this.params.slice()
|
|
|
|
this.$store.commit('removeParams', index)
|
|
this.$toast.error(this.$t('deleted'), {
|
|
icon: 'delete',
|
|
action: {
|
|
text: this.$t('undo'),
|
|
onClick: (e, toastObject) => {
|
|
this.params = oldParams
|
|
toastObject.remove()
|
|
},
|
|
},
|
|
})
|
|
},
|
|
addRequestBodyParam() {
|
|
this.$store.commit('addBodyParams', { key: '', value: '' })
|
|
return false
|
|
},
|
|
removeRequestBodyParam(index) {
|
|
// .slice() gives us an entirely new array rather than giving us just the reference
|
|
const oldBodyParams = this.bodyParams.slice()
|
|
|
|
this.$store.commit('removeBodyParams', index)
|
|
this.$toast.error(this.$t('deleted'), {
|
|
icon: 'delete',
|
|
action: {
|
|
text: this.$t('undo'),
|
|
onClick: (e, toastObject) => {
|
|
this.bodyParams = oldBodyParams
|
|
toastObject.remove()
|
|
},
|
|
},
|
|
})
|
|
},
|
|
copyRequest() {
|
|
if (navigator.share) {
|
|
const time = new Date().toLocaleTimeString()
|
|
const date = new Date().toLocaleDateString()
|
|
navigator
|
|
.share({
|
|
title: 'Postwoman',
|
|
text: `Postwoman • API request builder at ${time} on ${date}`,
|
|
url: window.location.href,
|
|
})
|
|
.then(() => {})
|
|
.catch(console.error)
|
|
} else {
|
|
const dummy = document.createElement('input')
|
|
document.body.appendChild(dummy)
|
|
dummy.value = window.location.href
|
|
dummy.select()
|
|
document.execCommand('copy')
|
|
document.body.removeChild(dummy)
|
|
this.$refs.copyRequest.innerHTML = this.doneButton
|
|
this.$toast.info(this.$t('copied_to_clipboard'), {
|
|
icon: 'done',
|
|
})
|
|
setTimeout(() => (this.$refs.copyRequest.innerHTML = this.copyButton), 1000)
|
|
}
|
|
},
|
|
copyRequestCode() {
|
|
this.$refs.copyRequestCode.innerHTML = this.doneButton
|
|
this.$toast.success(this.$t('copied_to_clipboard'), {
|
|
icon: 'done',
|
|
})
|
|
this.$refs.generatedCode.select()
|
|
document.execCommand('copy')
|
|
setTimeout(() => (this.$refs.copyRequestCode.innerHTML = this.copyButton), 1000)
|
|
},
|
|
ToggleExpandResponse() {
|
|
this.expandResponse = !this.expandResponse
|
|
this.responseBodyMaxLines = this.responseBodyMaxLines == Infinity ? 16 : Infinity
|
|
},
|
|
copyResponse() {
|
|
this.$refs.copyResponse.innerHTML = this.doneButton
|
|
this.$toast.success(this.$t('copied_to_clipboard'), {
|
|
icon: 'done',
|
|
})
|
|
const aux = document.createElement('textarea')
|
|
const copy =
|
|
this.responseType === 'application/json'
|
|
? JSON.stringify(this.response.body, null, 2)
|
|
: this.response.body
|
|
aux.innerText = copy
|
|
document.body.appendChild(aux)
|
|
aux.select()
|
|
document.execCommand('copy')
|
|
document.body.removeChild(aux)
|
|
setTimeout(() => (this.$refs.copyResponse.innerHTML = this.copyButton), 1000)
|
|
},
|
|
downloadResponse() {
|
|
const dataToWrite = JSON.stringify(this.response.body, null, 2)
|
|
const file = new Blob([dataToWrite], { type: this.responseType })
|
|
const a = document.createElement('a')
|
|
const url = URL.createObjectURL(file)
|
|
a.href = url
|
|
a.download = `${this.url + this.path} [${this.method}] on ${Date()}`.replace(/\./g, '[dot]')
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
this.$refs.downloadResponse.innerHTML = this.doneButton
|
|
this.$toast.success(this.$t('download_started'), {
|
|
icon: 'done',
|
|
})
|
|
setTimeout(() => {
|
|
document.body.removeChild(a)
|
|
window.URL.revokeObjectURL(url)
|
|
this.$refs.downloadResponse.innerHTML = this.downloadButton
|
|
}, 1000)
|
|
},
|
|
togglePreview() {
|
|
this.previewEnabled = !this.previewEnabled
|
|
if (this.previewEnabled) {
|
|
// If you want to add 'preview' support for other response types,
|
|
// just add them here.
|
|
if (this.responseType === 'text/html') {
|
|
// If the preview already has that URL loaded, let's not bother re-loading it all.
|
|
if (this.$refs.previewFrame.getAttribute('data-previewing-url') === this.url) return
|
|
// Use DOMParser to parse document HTML.
|
|
const previewDocument = new DOMParser().parseFromString(
|
|
this.response.body,
|
|
this.responseType
|
|
)
|
|
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
|
|
previewDocument.head.innerHTML =
|
|
`<base href="${this.url}">` + previewDocument.head.innerHTML
|
|
// Finally, set the iframe source to the resulting HTML.
|
|
this.$refs.previewFrame.srcdoc = previewDocument.documentElement.outerHTML
|
|
this.$refs.previewFrame.setAttribute('data-previewing-url', this.url)
|
|
}
|
|
}
|
|
},
|
|
setRouteQueryState() {
|
|
const flat = key => (this[key] !== '' ? `${key}=${this[key]}&` : '')
|
|
const deep = key => {
|
|
const haveItems = [...this[key]].length
|
|
if (haveItems && this[key]['value'] !== '') {
|
|
return `${key}=${JSON.stringify(this[key])}&`
|
|
}
|
|
return ''
|
|
}
|
|
let flats = [
|
|
'method',
|
|
'url',
|
|
'path',
|
|
!this.urlExcludes.auth ? 'auth' : null,
|
|
!this.urlExcludes.httpUser ? 'httpUser' : null,
|
|
!this.urlExcludes.httpPassword ? 'httpPassword' : null,
|
|
!this.urlExcludes.bearerToken ? 'bearerToken' : null,
|
|
'contentType',
|
|
]
|
|
.filter(item => item !== null)
|
|
.map(item => flat(item))
|
|
const deeps = ['headers', 'params'].map(item => deep(item))
|
|
const bodyParams = this.rawInput ? [flat('rawParams')] : [deep('bodyParams')]
|
|
|
|
history.replaceState(
|
|
window.location.href,
|
|
'',
|
|
`/?${encodeURI(
|
|
flats
|
|
.concat(deeps, bodyParams)
|
|
.join('')
|
|
.slice(0, -1)
|
|
)}`
|
|
)
|
|
},
|
|
setRouteQueries(queries) {
|
|
if (typeof queries !== 'object') throw new Error('Route query parameters must be a Object')
|
|
for (const key in queries) {
|
|
if (['headers', 'params', 'bodyParams'].includes(key))
|
|
this[key] = JSON.parse(decodeURI(queries[key]))
|
|
if (key === 'rawParams') {
|
|
this.rawInput = true
|
|
this.rawParams = queries['rawParams']
|
|
} else if (typeof this[key] === 'string') {
|
|
this[key] = queries[key]
|
|
}
|
|
}
|
|
},
|
|
observeRequestButton() {
|
|
const requestElement = this.$refs.request.$el
|
|
const sendButtonElement = this.$refs.sendButton
|
|
const observer = new IntersectionObserver(
|
|
(entries, observer) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) sendButtonElement.classList.remove('show')
|
|
// The button should float when it is no longer visible on screen.
|
|
// This is done by adding the show class to the button.
|
|
else sendButtonElement.classList.add('show')
|
|
})
|
|
},
|
|
{
|
|
rootMargin: '0px',
|
|
threshold: [0],
|
|
}
|
|
)
|
|
observer.observe(requestElement)
|
|
},
|
|
handleImport() {
|
|
const { value: text } = document.getElementById('import-text')
|
|
try {
|
|
const parsedCurl = parseCurlCommand(text)
|
|
const { origin, pathname } = new URL(parsedCurl.url.replace(/"/g, '').replace(/'/g, ''))
|
|
this.url = origin
|
|
this.path = pathname
|
|
this.headers = []
|
|
if (parsedCurl.headers) {
|
|
for (const key of Object.keys(parsedCurl.headers)) {
|
|
this.$store.commit('addHeaders', {
|
|
key,
|
|
value: parsedCurl.headers[key],
|
|
})
|
|
}
|
|
}
|
|
this.method = parsedCurl.method.toUpperCase()
|
|
if (parsedCurl['data']) {
|
|
this.rawInput = true
|
|
this.rawParams = parsedCurl['data']
|
|
}
|
|
this.showModal = false
|
|
} catch (error) {
|
|
this.showModal = false
|
|
this.$toast.error(this.$t('curl_invalid_format'), {
|
|
icon: 'error',
|
|
})
|
|
}
|
|
},
|
|
switchVisibility() {
|
|
this.passwordFieldType = this.passwordFieldType === 'password' ? 'text' : 'password'
|
|
},
|
|
clearContent(name, e) {
|
|
switch (name) {
|
|
case 'auth':
|
|
this.auth = 'None'
|
|
this.httpUser = ''
|
|
this.httpPassword = ''
|
|
this.bearerToken = ''
|
|
this.showTokenRequest = false
|
|
this.tokens = []
|
|
this.tokenReqs = []
|
|
break
|
|
case 'headers':
|
|
this.headers = []
|
|
break
|
|
case 'parameters':
|
|
this.params = []
|
|
break
|
|
case 'access_token':
|
|
this.accessTokenName = ''
|
|
this.oidcDiscoveryUrl = ''
|
|
this.authUrl = ''
|
|
this.accessTokenUrl = ''
|
|
this.clientId = ''
|
|
this.scope = ''
|
|
break
|
|
case 'tokens':
|
|
this.tokens = []
|
|
break
|
|
case 'tokenReqs':
|
|
this.tokenReqs = []
|
|
case 'tests':
|
|
this.testReports = null
|
|
break
|
|
default:
|
|
;(this.label = ''),
|
|
(this.method = 'GET'),
|
|
(this.url = 'https://httpbin.org'),
|
|
(this.auth = 'None'),
|
|
(this.path = '/get'),
|
|
(this.auth = 'None')
|
|
this.httpUser = ''
|
|
this.httpPassword = ''
|
|
this.bearerToken = ''
|
|
this.headers = []
|
|
this.params = []
|
|
this.bodyParams = []
|
|
this.rawParams = ''
|
|
this.showTokenRequest = false
|
|
this.tokens = []
|
|
this.tokenReqs = []
|
|
this.accessTokenName = ''
|
|
this.oidcDiscoveryUrl = ''
|
|
this.authUrl = ''
|
|
this.accessTokenUrl = ''
|
|
this.clientId = ''
|
|
this.scope = ''
|
|
this.files = []
|
|
}
|
|
e.target.innerHTML = this.doneButton
|
|
this.$toast.info(this.$t('cleared'), {
|
|
icon: 'clear_all',
|
|
})
|
|
setTimeout(() => (e.target.innerHTML = '<i class="material-icons">clear_all</i>'), 1000)
|
|
},
|
|
saveRequest() {
|
|
if (!this.checkCollections()) {
|
|
this.$toast.error(this.$t('create_collection'), {
|
|
icon: 'error',
|
|
})
|
|
return
|
|
}
|
|
|
|
this.editRequest = {
|
|
url: this.url,
|
|
path: this.path,
|
|
method: this.method,
|
|
auth: this.auth,
|
|
httpUser: this.httpUser,
|
|
httpPassword: this.httpPassword,
|
|
passwordFieldType: this.passwordFieldType,
|
|
bearerToken: this.bearerToken,
|
|
headers: this.headers,
|
|
params: this.params,
|
|
bodyParams: this.bodyParams,
|
|
rawParams: this.rawParams,
|
|
rawInput: this.rawInput,
|
|
contentType: this.contentType,
|
|
requestType: this.requestType,
|
|
}
|
|
|
|
if (this.selectedRequest.url) {
|
|
this.editRequest = Object.assign({}, this.selectedRequest, this.editRequest)
|
|
}
|
|
|
|
this.showRequestModal = true
|
|
},
|
|
hideRequestModal() {
|
|
this.showRequestModal = false
|
|
this.editRequest = {}
|
|
},
|
|
setExclude(excludedField, excluded) {
|
|
if (excludedField === 'auth') {
|
|
this.urlExcludes.auth = excluded
|
|
this.urlExcludes.httpUser = excluded
|
|
this.urlExcludes.httpPassword = excluded
|
|
this.urlExcludes.bearerToken = excluded
|
|
} else {
|
|
this.urlExcludes[excludedField] = excluded
|
|
}
|
|
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}<br/>${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',
|
|
})
|
|
}
|
|
},
|
|
async handleAccessTokenRequest() {
|
|
if (this.oidcDiscoveryUrl === '' && (this.authUrl === '' || this.accessTokenUrl === '')) {
|
|
this.$toast.error(this.$t('complete_config_urls'), {
|
|
icon: 'error',
|
|
})
|
|
return
|
|
}
|
|
try {
|
|
const tokenReqParams = {
|
|
grantType: 'code',
|
|
oidcDiscoveryUrl: this.oidcDiscoveryUrl,
|
|
authUrl: this.authUrl,
|
|
accessTokenUrl: this.accessTokenUrl,
|
|
clientId: this.clientId,
|
|
scope: this.scope,
|
|
}
|
|
await tokenRequest(tokenReqParams)
|
|
} catch (e) {
|
|
this.$toast.error(e, {
|
|
icon: 'code',
|
|
})
|
|
}
|
|
},
|
|
async oauthRedirectReq() {
|
|
const tokenInfo = await oauthRedirect()
|
|
if (tokenInfo.hasOwnProperty('access_token')) {
|
|
this.bearerToken = tokenInfo.access_token
|
|
this.addOAuthToken({
|
|
name: this.accessTokenName,
|
|
value: tokenInfo.access_token,
|
|
})
|
|
}
|
|
},
|
|
addOAuthToken({ name, value }) {
|
|
this.$store.commit('addOAuthToken', {
|
|
name,
|
|
value,
|
|
})
|
|
return false
|
|
},
|
|
removeOAuthToken(index) {
|
|
const oldTokens = this.tokens.slice()
|
|
this.$store.commit('removeOAuthToken', index)
|
|
this.$toast.error(this.$t('deleted'), {
|
|
icon: 'delete',
|
|
action: {
|
|
text: this.$t('undo'),
|
|
onClick: (e, toastObject) => {
|
|
this.tokens = oldTokens
|
|
toastObject.remove()
|
|
},
|
|
},
|
|
})
|
|
},
|
|
useOAuthToken(value) {
|
|
this.bearerToken = value
|
|
this.showTokenList = false
|
|
},
|
|
addOAuthTokenReq() {
|
|
try {
|
|
const name = this.tokenReqName
|
|
const details = JSON.parse(this.tokenReqDetails)
|
|
this.$store.commit('addOAuthTokenReq', {
|
|
name,
|
|
details,
|
|
})
|
|
this.$toast.info(this.$t('token_request_saved'))
|
|
this.showTokenRequestList = false
|
|
} catch (e) {
|
|
this.$toast.error(e, {
|
|
icon: 'code',
|
|
})
|
|
}
|
|
},
|
|
removeOAuthTokenReq(index) {
|
|
const oldTokenReqs = this.tokenReqs.slice()
|
|
const targetReqIndex = this.tokenReqs.findIndex(({ name }) => name === this.tokenReqName)
|
|
if (targetReqIndex < 0) return
|
|
this.$store.commit('removeOAuthTokenReq', targetReqIndex)
|
|
this.$toast.error(this.$t('deleted'), {
|
|
icon: 'delete',
|
|
action: {
|
|
text: this.$t('undo'),
|
|
onClick: (e, toastObject) => {
|
|
this.tokenReqs = oldTokenReqs
|
|
toastObject.remove()
|
|
},
|
|
},
|
|
})
|
|
},
|
|
tokenReqChange({ target }) {
|
|
const { details, name } = this.tokenReqs.find(({ name }) => name === target.value)
|
|
const { oidcDiscoveryUrl, authUrl, accessTokenUrl, clientId, scope } = details
|
|
this.tokenReqName = name
|
|
this.oidcDiscoveryUrl = oidcDiscoveryUrl
|
|
this.authUrl = authUrl
|
|
this.accessTokenUrl = accessTokenUrl
|
|
this.clientId = clientId
|
|
this.scope = scope
|
|
},
|
|
},
|
|
async mounted() {
|
|
this.observeRequestButton()
|
|
this._keyListener = function(e) {
|
|
if (e.key === 'g' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault()
|
|
this.sendRequest()
|
|
} else if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault()
|
|
this.saveRequest()
|
|
} else if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault()
|
|
this.copyRequest()
|
|
} else if (e.key === 'j' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault()
|
|
this.$refs.clearAll.click()
|
|
} else if (e.key === 'Escape') {
|
|
e.preventDefault()
|
|
this.showModal = this.showTokenList = this.showTokenRequestList = this.showRequestModal = false
|
|
this.isHidden = true
|
|
}
|
|
}
|
|
document.addEventListener('keydown', this._keyListener.bind(this))
|
|
await this.oauthRedirectReq()
|
|
},
|
|
created() {
|
|
this.urlExcludes = this.$store.state.postwoman.settings.URL_EXCLUDES || {
|
|
// Exclude authentication by default for security reasons.
|
|
auth: true,
|
|
httpUser: true,
|
|
httpPassword: true,
|
|
bearerToken: true,
|
|
}
|
|
|
|
if (Object.keys(this.$route.query).length) this.setRouteQueries(this.$route.query)
|
|
this.$watch(
|
|
vm => [
|
|
vm.label,
|
|
vm.method,
|
|
vm.url,
|
|
vm.auth,
|
|
vm.path,
|
|
vm.httpUser,
|
|
vm.httpPassword,
|
|
vm.bearerToken,
|
|
vm.headers,
|
|
vm.params,
|
|
vm.bodyParams,
|
|
vm.contentType,
|
|
vm.rawParams,
|
|
],
|
|
val => {
|
|
this.setRouteQueryState()
|
|
}
|
|
)
|
|
},
|
|
beforeDestroy() {
|
|
document.removeEventListener('keydown', this._keyListener)
|
|
},
|
|
}
|
|
</script>
|