Merge branch 'master' into feat/collections

This commit is contained in:
Liyas Thomas
2019-10-16 18:12:35 +05:30
committed by GitHub
36 changed files with 4542 additions and 764 deletions

View File

@@ -3,6 +3,10 @@ Dockerfile
.github
# Created by .ignore support plugin (hsz.mobi)
# Firebase
.firebase
### Node template
# Logs
logs
@@ -97,4 +101,4 @@ sw.*
.postwoman
# File explorer
.directory
.directory

14
.firebaserc Normal file
View File

@@ -0,0 +1,14 @@
{
"projects": {
"default": "postwoman-api"
},
"targets": {
"postwoman-api": {
"hosting": {
"postwoman": [
"postwoman"
]
}
}
}
}

6
.gitignore vendored
View File

@@ -1,4 +1,8 @@
# Created by .ignore support plugin (hsz.mobi)
# Firebase
.firebase
### Node template
# Logs
logs
@@ -93,4 +97,4 @@ sw.*
.postwoman
# File explorer
.directory
.directory

View File

@@ -15,30 +15,39 @@ language: node_js
node_js:
- "12"
addons:
apt:
packages:
- libgconf-2-4 # cypress binary dependency
env:
- DEPLOY_ENV=POSTWOMAN_IO
cache:
npm: true
directories:
- "node_modules"
- ~/.cache
branches:
only:
- "master"
install:
- "npm install firebase-tools"
- "npm install"
before_script:
- "npm run test"
script:
- "cd functions"
- "npm install"
- "cd .."
- "npm run generate"
notifications:
webhooks: https://www.travisbuddy.com
deploy:
provider: pages
skip-cleanup: true
# Refer to: https://docs.travis-ci.com/user/deployment/pages/#Setting-the-GitHub-token
github-token: $GITHUB_ACCESS_TOKEN
target-branch: gh-pages
local-dir: dist
on:
branch: master
after_success:
- firebase deploy --token $FIREBASE_TOKEN

View File

@@ -5,13 +5,13 @@ When I wrote this, only God and I understood what I was doing. Now, only God kno
<div align="center">
<a href="https://liyas-thomas.firebaseapp.com"><img src="static/icons/logo.svg" alt="Liyas Thomas" height="128"></a>
<br>
<h1><a href="https://postwoman.io">Postwoman</a></h1>
<h1><a href="https://postwoman.io">Postwoman.io</a></h1>
<p>
API request builder - Helps you create your requests faster, saving you precious time on your development. <a href="https://postwoman.launchaco.com">Subscribe for updates</a>
API request builder - Helps you create your requests faster, saving you precious time on your development - <a href="https://postwoman.launchaco.com">Subscribe for updates</a>
</p>
<p>
[![Financial Contributors on Open Collective](https://opencollective.com/postwoman/all/badge.svg?label=financial+contributors)](https://opencollective.com/postwoman) [![Build Status](https://travis-ci.com/liyasthomas/postwoman.svg?branch=master)](https://travis-ci.com/liyasthomas/postwoman) [![GitHub release](https://img.shields.io/github/release/liyasthomas/postwoman/all.svg)](https://github.com/liyasthomas/postwoman/releases/latest) [![repo size](https://img.shields.io/github/repo-size/liyasthomas/postwoman.svg)](https://github.com/liyasthomas/postwoman/archive/master.zip) [![license](https://img.shields.io/github/license/liyasthomas/postwoman.svg)](https://github.com/liyasthomas/postwoman/blob/master/LICENSE) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/liyasthomas/postwoman/issues) [![Website](https://img.shields.io/website-up-down-green-red/https/shields.io.svg?label=website)](https://postwoman.io) [![Donate](https://img.shields.io/badge/$-donate-blue.svg)](https://www.paypal.me/liyascthomas) [![Buy me a coffee](https://img.shields.io/badge/$-BuyMeACoffee-orange.svg)](https://www.buymeacoffee.com/liyasthomas) [![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-blueviolet)](https://t.me/postwoman_app) [![Chat on Discord](https://img.shields.io/badge/chat-Discord-violet?logo=discord)](https://discord.gg/GAMWxmR)
[![Build Status](https://travis-ci.com/liyasthomas/postwoman.svg?branch=master)](https://travis-ci.com/liyasthomas/postwoman) [![GitHub release](https://img.shields.io/github/release/liyasthomas/postwoman/all.svg)](https://github.com/liyasthomas/postwoman/releases/latest) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/liyasthomas/postwoman/issues) [![Website](https://img.shields.io/website-up-down-green-red/https/shields.io.svg?label=website)](https://postwoman.io) [![Financial Contributors on Open Collective](https://opencollective.com/postwoman/all/badge.svg?label=financial+contributors)](https://opencollective.com/postwoman) [![Donate](https://img.shields.io/badge/$-donate-blue.svg)](https://www.paypal.me/liyascthomas) [![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-blueviolet)](https://t.me/postwoman_app) [![Chat on Discord](https://img.shields.io/badge/chat-Discord-violet?logo=discord)](https://discord.gg/GAMWxmR)
</p>
<sub>Built with ❤︎ by
@@ -26,7 +26,7 @@ When I wrote this, only God and I understood what I was doing. Now, only God kno
**Chat here: _[Telegram](https://t.me/postwoman_app), [Discord](https://discord.gg/GAMWxmR)_**
**Donate here: _[PayPal](https://www.paypal.me/liyascthomas), [Buy me a coffee](https://www.buymeacoffee.com/liyasthomas)_**
**Donate here: _[PayPal](https://www.paypal.me/liyascthomas), [Open Collective](https://opencollective.com/postwoman), [Patreon](https://www.patreon.com/liyasthomas)_**
<div align="center">
<br>
@@ -34,13 +34,13 @@ When I wrote this, only God and I understood what I was doing. Now, only God kno
<br>
</div>
### Features :sparkles:
### Features
:heart: **Lightweight**: Crafted with minimalistic UI design
❤️ **Lightweight**: Crafted with minimalistic UI design. Simple design is the best design.
- Faster, lighter, cleaner, minimal & responsive
:zap: **Real-time**: Send requests and get/copy responses right away!
⚡️ **Fast**: Send requests and get/copy responses in real-time! Fast software is the best software.
**Methods:**
- `GET` - Retrieve information about the REST API resource
@@ -53,70 +53,82 @@ When I wrote this, only God and I understood what I was doing. Now, only God kno
_History entries are synced with local session storage_
:rainbow: **VIBGYOR**: Neon combination background, foreground & accent colors - because customization === freedom :sparkles:
🌈 **Make it yours**: Customizable combinations for background, foreground and accent colors: because customization === freedom. [Customize now ✨](https://postwoman.io/settings).
**Customizations:**
- Dark and Light background themes
- Choose accent color
- Choose theme: Kinda Dark (default), Clearly White, Just Black and System theme
- Choose accent color: Green (default), Yellow, Pink, Red, Purple, Orange, Cyan and Blue
- Toggle multi-colored frames
_Customized themes are also synced with local session storage_
:fire: **PWA**: Install as a **[PWA](https://developers.google.com/web/progressive-web-apps)** on your device
🔥 **PWA**: Install as a **[PWA](https://developers.google.com/web/progressive-web-apps)** on your device.
**Features:**
- Instant loading with Service Workers
- Instant loading with [Service Workers](https://developers.google.com/web/fundamentals/primers/service-workers)
- Offline support
- Low RAM/memory and CPU usage
- [Add to Home Screen](https://developers.google.com/web/fundamentals/app-install-banners) (button in footer)
- [Desktop PWA](https://developers.google.com/web/progressive-web-apps/desktop) support (button in footer)
- [Full features](https://developers.google.com/web/progressive-web-apps)
:rocket: **Request**: Retrieve data from a URL without having to do a full page refresh
🚀 **Request**: Retrieve data from a URL without having to do a full page refresh.
- Choose `method`
- Enter `URL`
- Enter `Path`
- Copy public "Share URL"
- Generate request code for JavaScript XHR, Fetch, cURL
**Features:**
- Copy/share public "Share URL"
- Generate request code for JavaScript XHR, Fetch and cURL
- Copy generated request code to clipboard
- Import cURL
- Label requests
:electric_plug: **Web Socket**: Establish full-duplex communication channels over a single TCP connection
🔌 **Web Socket**: Establish full-duplex communication channels over a single TCP connection.
- Send and receive data
:closed_lock_with_key: **Authentication**: Allows to identity the end user
🔐 **Authentication**: Allows to identity the end user.
**Types:**
- None
- Basic authentication using username and password
- Token based authentication
:loudspeaker: **Headers**: Describes the format the body of your request is being sent as
📢 **Headers**: Describes the format the body of your request is being sent as.
:mailbox: **Parameters**: Use request parameters to set varying parts in simulated requests
- Add or remove Header list
:page_with_curl: **Request Body**: Used to send and receive data via the REST API
📫 **Parameters**: Use request parameters to set varying parts in simulated requests.
📃 **Request Body**: Used to send and receive data via the REST API.
**Options:**
- Set Content Type
- Toggle between RAW input and parameter list
- Add or remove Parameter list
- Toggle between key-value and RAW input Parameter list
:wave: **Responses**: Contains the status line, headers and the message/response body
👋 **Responses**: Contains the status line, headers and the message/response body.
- Copy response to clipboard
- View preview for HTML responses
_HTML responses have "Preview HTML" feature_
:alarm_clock: **History**: Request entries are synced with local session storage to reuse with a single click
**History**: Request entries are synced with local session storage to reuse with a single click.
**Fields**
- Label
- Timestamp
- Method
- Status code
- URL
- Path
_History entries can be deleted one-by-one or all together_
_History entries can be sorted by any fields_
_Histories can deleted one-by-one or all together_
## Demo 🚀
@@ -167,7 +179,7 @@ docker run -p 3000:3000 liyasthomas/postwoman:latest
docker build -t postwoman:latest
```
## Releasing 🔖
## Releasing 🏷️
1. [Clone this repo](https://help.github.com/en/articles/cloning-a-repository) with git.
1. Install dependencies by running `npm install` within the directory that you cloned (probably `postwoman`).
@@ -229,6 +241,7 @@ See the list of [contributors](https://github.com/liyasthomas/postwoman/graphs/c
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/liyasthomas/postwoman/graphs/contributors"><img src="https://opencollective.com/postwoman/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
@@ -237,7 +250,7 @@ Become a financial contributor and help us sustain our community. [[Contribute](
#### Individuals
<a href="https://opencollective.com/postwoman"><img src="https://opencollective.com/postwoman/individuals.svg?width=890"></a>
<a href="https://opencollective.com/postwoman"><img src="https://opencollective.com/postwoman/individuals.svg"></a>
#### Organizations

View File

@@ -1,2 +1,2 @@
// Poppins (Google Fonts)
@import url("https://fonts.googleapis.com/css?family=Material+Icons|Poppins:400,600&display=swap");
@import url('https://fonts.googleapis.com/css?family=Material+Icons|Poppins:500,700|Roboto+Mono:400&display=swap');

View File

@@ -34,13 +34,13 @@ a {
display: inline-flex;
color: inherit;
text-decoration: none;
font-weight: 600;
font-weight: 700;
}
body {
background-color: var(--bg-color);
color: var(--fg-color);
font-weight: 400;
font-weight: 500;
font-size: 16px;
font-family: "Poppins", "Roboto", "Noto", sans-serif;
line-height: 1.5;
@@ -53,7 +53,7 @@ h1,
h2,
h3 {
margin: 0;
font-weight: 600;
font-weight: 700;
}
h3.title {
@@ -78,10 +78,7 @@ nav {
}
body.sticky-footer footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
opacity: .25;
}
.logo {
@@ -97,7 +94,7 @@ button {
border-radius: 20px;
background-color: var(--ac-color);
color: var(--act-color);
font-weight: 600;
font-weight: 700;
font-size: 16px;
font-family: "Poppins", "Roboto", "Noto", sans-serif;
transition: all 0.2s ease-in-out;
@@ -140,7 +137,7 @@ legend {
align-items: center;
justify-content: center;
color: var(--fg-color);
font-weight: 600;
font-weight: 700;
cursor: pointer;
i {
@@ -148,11 +145,6 @@ legend {
}
}
fieldset textarea,
fieldset pre code {
resize: vertical;
}
fieldset.blue legend {
color: #57b5f9;
}
@@ -204,11 +196,13 @@ pre {
background-color: var(--bg-dark-color);
color: var(--fg-color);
font-size: 16px;
font-family: monospace;
font-family: 'Roboto Mono', monospace;
transition: all 0.2s ease-in-out;
user-select: text;
width: calc(100% - 8px);
min-height: 40px;
resize: vertical;
text-overflow: ellipsis;
&:not([readonly]):hover {
background-color: var(--bg-dark-color);
@@ -228,6 +222,7 @@ code {
.hljs-subst {
background-color: var(--bg-dark-color) !important;
color: var(--fg-color) !important;
font-family: 'Roboto Mono', monospace;
}
select,
@@ -306,6 +301,10 @@ ol li {
justify-content: space-between;
}
.show-on-small-screen {
display: flex;
}
@media (max-width: $responsiveWidth) {
header div {
display: flex;
@@ -331,6 +330,10 @@ ol li {
.hide-on-small-screen {
display: none;
}
.show-on-small-screen {
display: inline-flex;
}
}
#installPWA {
@@ -373,7 +376,7 @@ fieldset#history {
position: absolute;
top: 12px;
right: 12px;
font-family: monospace, monospace;
font-family: 'Roboto Mono', monospace;
}
}
}

View File

@@ -3,6 +3,8 @@
- dark (default)
- light
- black
- auto
*/
// Dark is the default theme variant.
@@ -29,9 +31,9 @@
:root.light {
// Dark Background color
--bg-dark-color: #e8f0fe;
--bg-dark-color: #f6f6f6;
// Background color
--bg-color: #fff;
--bg-color: #ffffff;
// Auto-complete color
--atc-color: #ebebeb;
// Text color
@@ -39,11 +41,78 @@
// Light Text color
--fg-light-color: rgb(150, 155, 160);
// Border color
--brd-color: #f2f2f2;
--brd-color: #eeeeed;
// Error color
--err-color: invert(#303341, 1);
--err-color: #f6f6f6;
// Acent color
--ac-color: #57b5f9;
// Active text color
--act-color: #fff;
--act-color: #ffffff;
}
:root.black {
// Dark Background color
--bg-dark-color: rgb(8, 8, 8);
// Background color
--bg-color: #000000;
// Auto-complete color
--atc-color: rgb(18, 18, 18);
// Text color
--fg-color: rgb(250, 250, 250);
// Light Text color
--fg-light-color: rgb(100, 100, 100);
// Border color
--brd-color: rgb(16, 16, 16);
// Error color
--err-color: rgb(8, 8, 8);
// Acent color
--ac-color: #50fa7b;
// Active text color
--act-color: #000000;
}
@media(prefers-color-scheme: light) {
:root.auto {
// Dark Background color
--bg-dark-color: #f6f6f6;
// Background color
--bg-color: #ffffff;
// Auto-complete color
--atc-color: #ebebeb;
// Text color
--fg-color: #525252;
// Light Text color
--fg-light-color: rgb(150, 155, 160);
// Border color
--brd-color: #eeeeed;
// Error color
--err-color: #f6f6f6;
// Acent color
--ac-color: #57b5f9;
// Active text color
--act-color: #ffffff;
}
}
@media(prefers-color-scheme: dark) {
:root.auto {
// Dark Background color
--bg-dark-color: rgb(41, 42, 45);
// Background color
--bg-color: rgb(37, 38, 40);
// Auto-complete color
--atc-color: rgb(49, 49, 55);
// Text color
--fg-color: rgb(247, 248, 248);
// Light Text color
--fg-light-color: rgb(150, 155, 160);
// Border color
--brd-color: rgb(48, 47, 55);
// Error color
--err-color: rgb(41, 42, 45);
// Acent color
--ac-color: #50fa7b;
// Active text color
--act-color: rgb(37, 38, 40);
}
}

View File

@@ -1,181 +1,204 @@
<template>
<div class="autocomplete-wrapper">
<label>
<input type="text" :placeholder="placeholder" v-model="value" @input="updateSuggestions" @keyup="updateSuggestions" @click="updateSuggestions" @keydown="handleKeystroke" ref="acInput" :spellcheck="spellcheck" :autocapitalize="spellcheck" :autocorrect="spellcheck">
<ul class="suggestions" v-if="suggestions.length > 0 && suggestionsVisible" :style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }">
<li v-for="(suggestion, index) in suggestions" @click.prevent="forceSuggestion(suggestion)" :class="{ active: currentSuggestionIndex === index }" :key="index">{{ suggestion }}</li>
</ul>
</label>
<input
type="text"
:placeholder="placeholder"
v-model="value"
@input="updateSuggestions"
@keyup="updateSuggestions"
@click="updateSuggestions"
@keydown="handleKeystroke"
ref="acInput"
:spellcheck="spellcheck"
:autocapitalize="spellcheck"
:autocorrect="spellcheck"
/>
<ul
class="suggestions"
v-if="suggestions.length > 0 && suggestionsVisible"
:style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }"
>
<li
v-for="(suggestion, index) in suggestions"
@click.prevent="forceSuggestion(suggestion)"
:class="{ active: currentSuggestionIndex === index }"
:key="index"
>{{ suggestion }}</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.autocomplete-wrapper {
position: relative;
.autocomplete-wrapper {
position: relative;
input:focus+ul.suggestions,
ul.suggestions:hover {
input:focus + ul.suggestions,
ul.suggestions:hover {
display: block;
}
ul.suggestions {
display: none;
background-color: var(--atc-color);
position: absolute;
top: calc(100% - 4px);
margin: 0 4px;
left: 0;
padding: 0;
border-radius: 0 0 4px 4px;
z-index: 9999;
transition: transform 200ms ease-out;
li {
width: 100%;
display: block;
}
padding: 8px 16px;
font-size: 18px;
font-family: 'Roboto Mono', monospace;
white-space: pre-wrap;
ul.suggestions {
display: none;
background-color: var(--atc-color);
position: absolute;
top: 90%;
margin: 0 4px;
left: 0;
padding: 0;
border-radius: 0 0 4px 4px;
z-index: 9999;
transition: transform 200ms ease-out;
&:last-child {
border-radius: 0 0 4px 4px;
}
li {
width: 100%;
display: block;
padding: 8px 16px;
font-weight: 700;
font-size: 18px;
font-family: monospace;
white-space: pre-wrap;
&:last-child {
border-radius: 0 0 4px 4px;
}
&:hover,
&.active {
background-color: var(--ac-color);
color: var(--act-color);
cursor: pointer;
}
&:hover,
&.active {
background-color: var(--ac-color);
color: var(--act-color);
cursor: pointer;
}
}
}
}
</style>
<script>
const KEY_TAB = 9;
const KEY_ESC = 27;
const KEY_TAB = 9;
const KEY_ESC = 27;
const KEY_ARROW_UP = 38;
const KEY_ARROW_DOWN = 40;
const KEY_ARROW_UP = 38;
const KEY_ARROW_DOWN = 40;
export default {
props: {
spellcheck: {
type: Boolean,
default: true,
required: false
},
placeholder: {
type: String,
default: 'Start typing...',
required: false
},
source: {
type: Array,
required: true
}
export default {
props: {
spellcheck: {
type: Boolean,
default: true,
required: false
},
watch: {
value() {
this.$emit('input', this.value);
}
placeholder: {
type: String,
default: "Start typing...",
required: false
},
data() {
return {
value: "application/json",
selectionStart: 0,
suggestionsOffsetLeft: 0,
currentSuggestionIndex: -1,
suggestionsVisible: false
}
},
source: {
type: Array,
required: true
}
},
methods: {
updateSuggestions(event) {
// Hide suggestions if ESC pressed.
if (event.which && event.which === KEY_ESC) {
event.preventDefault();
this.suggestionsVisible = false;
this.currentSuggestionIndex = -1;
return;
}
watch: {
value() {
this.$emit("input", this.value);
}
},
// As suggestions is a reactive property, this implicitly
// causes suggestions to update.
this.selectionStart = this.$refs.acInput.selectionStart;
this.suggestionsOffsetLeft = (12 * this.selectionStart);
this.suggestionsVisible = true;
},
data() {
return {
value: "application/json",
selectionStart: 0,
suggestionsOffsetLeft: 0,
currentSuggestionIndex: -1,
suggestionsVisible: false
};
},
forceSuggestion(text) {
let input = this.value.substring(0, this.selectionStart);
this.value = input + text;
this.selectionStart = this.value.length;
this.suggestionsVisible = true;
methods: {
updateSuggestions(event) {
// Hide suggestions if ESC pressed.
if (event.which && event.which === KEY_ESC) {
event.preventDefault();
this.suggestionsVisible = false;
this.currentSuggestionIndex = -1;
},
return;
}
handleKeystroke(event) {
if (event.which === KEY_ARROW_UP) {
// As suggestions is a reactive property, this implicitly
// causes suggestions to update.
this.selectionStart = this.$refs.acInput.selectionStart;
this.suggestionsOffsetLeft = 12 * this.selectionStart;
this.suggestionsVisible = true;
},
forceSuggestion(text) {
let input = this.value.substring(0, this.selectionStart);
this.value = input + text;
this.selectionStart = this.value.length;
this.suggestionsVisible = true;
this.currentSuggestionIndex = -1;
},
handleKeystroke(event) {
switch (event.which) {
case KEY_ARROW_UP:
event.preventDefault();
this.currentSuggestionIndex =this.currentSuggestionIndex - 1 >= 0 ? this.currentSuggestionIndex - 1 : 0;
break;
this.currentSuggestionIndex = this.currentSuggestionIndex - 1 >= 0 ?
this.currentSuggestionIndex - 1 :
0;
} else if (event.which === KEY_ARROW_DOWN) {
case KEY_ARROW_DOWN:
event.preventDefault();
this.currentSuggestionIndex = this.currentSuggestionIndex < this.suggestions.length - 1 ? this.currentSuggestionIndex + 1
: this.suggestions.length - 1;
break;
this.currentSuggestionIndex = this.currentSuggestionIndex < this.suggestions.length - 1 ?
this.currentSuggestionIndex + 1 :
this.suggestions.length - 1;
}
if (event.which === KEY_TAB) {
case KEY_TAB:
event.preventDefault();
let activeSuggestion = this.suggestions[this.currentSuggestionIndex >= 0 ? this.currentSuggestionIndex : 0];
if (activeSuggestion) {
let input = this.value.substring(0, this.selectionStart);
this.value = input + activeSuggestion;
}
}
break;
default:
break;
}
},
}
},
computed: {
/**
* Gets the suggestions list to be displayed under the input box.
*
* @returns {default.props.source|{type, required}}
*/
suggestions() {
let input = this.value.substring(0, this.selectionStart);
computed: {
/**
* Gets the suggestions list to be displayed under the input box.
*
* @returns {default.props.source|{type, required}}
*/
suggestions() {
let input = this.value.substring(0, this.selectionStart);
return this.source.filter((entry) => {
return entry.toLowerCase().startsWith(input.toLowerCase()) &&
input.toLowerCase() !== entry.toLowerCase();
return (
this.source
.filter(entry => {
return (
entry.toLowerCase().startsWith(input.toLowerCase()) &&
input.toLowerCase() !== entry.toLowerCase()
);
})
// Cut off the part that's already been typed.
.map((entry) => entry.substring(this.selectionStart))
.map(entry => entry.substring(this.selectionStart))
// We only want the top 3 suggestions.
.slice(0, 3);
}
},
mounted() {
this.updateSuggestions({
target: this.$refs.acInput
});
.slice(0, 3)
);
}
}
},
mounted() {
this.updateSuggestions({
target: this.$refs.acInput
});
}
};
</script>

View File

@@ -6,11 +6,14 @@
</li>
</ul>
<ul>
<li @click="sort_by_label()">
<label for="" class="flex-wrap">Label<i class="material-icons">sort</i></label>
</li>
<li @click="sort_by_time()">
<label for="" class="flex-wrap">Time<i class="material-icons">sort</i></label>
</li>
<li @click="sort_by_status_code()">
<label for="" class="flex-wrap">Status Code<i class="material-icons">sort</i></label>
<label for="" class="flex-wrap">Status<i class="material-icons">sort</i></label>
</li>
<li @click="sort_by_url()">
<label for="" class="flex-wrap">URL<i class="material-icons">sort</i></label>
@@ -21,6 +24,9 @@
</ul>
<virtual-list class="virtual-list" :class="{filled: filteredHistory.length}" :size="54" :remain="Math.min(5, filteredHistory.length)">
<ul v-for="(entry, index) in filteredHistory" :key="index" class="entry">
<li>
<input aria-label="Label" type="text" readonly :value="entry.label" placeholder="No label">
</li>
<li>
<input aria-label="Time" type="text" readonly :value="entry.time" :title="entry.date">
</li>
@@ -32,7 +38,7 @@
<input aria-label="URL" type="text" readonly :value="entry.url">
</li>
<li>
<input aria-label="Path" type="text" readonly :value="entry.path">
<input aria-label="Path" type="text" readonly :value="entry.path" placeholder="No path">
</li>
<div class="show-on-small-screen">
<li>
@@ -101,6 +107,7 @@
filterText: '',
showFilter: false,
isClearingHistory: false,
reverse_sort_label: false,
reverse_sort_time: false,
reverse_sort_status_code: false,
reverse_sort_url: false,
@@ -198,6 +205,17 @@
this.history = byUrl;
this.reverse_sort_url = !this.reverse_sort_url;
},
sort_by_label() {
let byLabel = this.history.slice(0);
byLabel.sort((a, b)=>{
if(this.reverse_sort_label)
return a.label == b.label ? 0 : +(a.label < b.label) || -1;
else
return a.label == b.label ? 0 : +(a.label > b.label) || -1;
});
this.history = byLabel;
this.reverse_sort_label = !this.reverse_sort_label;
},
sort_by_path() {
let byPath = this.history.slice(0);
byPath.sort((a, b)=>{

View File

@@ -37,7 +37,7 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
color: #ffffff;
}
}
}

View File

@@ -4,21 +4,22 @@
<span class="handle"></span>
</label>
<label class="caption">
<slot /></label>
<slot/>
</label>
</div>
</template>
<style lang="scss" scoped>
$useBorder: true;
$borderColor: var(--fg-color);
$useBorder: false;
$borderColor: var(--fg-light-color);
$activeColor: var(--ac-color);
$inactiveColor: var(--fg-color);
$inactiveColor: var(--fg-light-color);
$inactiveHandleColor: $inactiveColor;
$inactiveHandleColor: var(--bg-color);
$activeHandleColor: var(--act-color);
$width: 50px;
$height: 20px;
$width: 32px;
$height: 16px;
$handleSpacing: 4px;
$transition: all 0.2s ease-in-out;
@@ -29,7 +30,6 @@
}
label.caption {
margin-left: 4px;
vertical-align: middle;
cursor: pointer;
}
@@ -43,11 +43,11 @@
background-color: if($useBorder, transparent, $inactiveColor);
vertical-align: middle;
border-radius: 100px;
border-radius: 32px;
transition: $transition;
box-sizing: initial;
padding: 0;
margin: 10px 5px;
margin: 8px 4px;
cursor: pointer;
.handle {

9
cypress.json Normal file
View File

@@ -0,0 +1,9 @@
{
"baseUrl": "http://localhost:3000",
"integrationFolder": "tests/e2e/integration",
"screenshotsFolder": "tests/e2e/screenshots",
"fixturesFolder": "tests/e2e/fixtures",
"supportFile": "tests/e2e/support",
"pluginsFile": false,
"video": false
}

7
database.rules.json Normal file
View File

@@ -0,0 +1,7 @@
{
/* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
"rules": {
".read": false,
".write": false
}
}

22
firebase.json Normal file
View File

@@ -0,0 +1,22 @@
{
"database": {
"rules": "database.rules.json"
},
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"hosting": {
"target": "postwoman",
"public": "dist",
"cleanUrls": true,
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
},
"storage": {
"rules": "storage.rules"
}
}

26
firestore.indexes.json Normal file
View File

@@ -0,0 +1,26 @@
{
// Example:
//
// "indexes": [
// {
// "collectionGroup": "widgets",
// "queryScope": "COLLECTION",
// "fields": [
// { "fieldPath": "foo", "arrayConfig": "CONTAINS" },
// { "fieldPath": "bar", "mode": "DESCENDING" }
// ]
// },
//
// "fieldOverrides": [
// {
// "collectionGroup": "widgets",
// "fieldPath": "baz",
// "indexes": [
// { "order": "ASCENDING", "queryScope": "COLLECTION" }
// ]
// },
// ]
// ]
"indexes": [],
"fieldOverrides": []
}

7
firestore.rules Normal file
View File

@@ -0,0 +1,7 @@
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}

1
functions/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

8
functions/index.js Normal file
View File

@@ -0,0 +1,8 @@
const functions = require('firebase-functions');
// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
// exports.helloWorld = functions.https.onRequest((request, response) => {
// response.send("Hello from Firebase!");
// });

1915
functions/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
functions/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase serve --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "8"
},
"dependencies": {
"firebase-admin": "^8.0.0",
"firebase-functions": "^3.1.0"
},
"devDependencies": {
"firebase-functions-test": "^0.1.6"
},
"private": true
}

View File

@@ -174,7 +174,7 @@
let vibrant = this.$store.state.postwoman.settings.THEME_COLOR_VIBRANT;
if (vibrant == null) vibrant = true;
document.documentElement.style.setProperty('--ac-color', color);
document.documentElement.style.setProperty('--act-color', vibrant ? 'rgb(37, 38, 40)' : '#fff');
document.documentElement.style.setProperty('--act-color', vibrant ? 'rgb(37, 38, 40)' : '#ffffff');
})();
},
@@ -183,6 +183,23 @@
// etc.
(async () => {
this.showInstallPrompt = await intializePwa();
let cookiesAllowed = localStorage.getItem('cookiesAllowed') === 'yes';
if(!cookiesAllowed) {
this.$toast.show('We use cookies', {
icon: 'info',
duration: 5000,
theme: 'toasted-primary',
action: [
{
text: 'Dismiss',
onClick: (e, toastObject) => {
localStorage.setItem('cookiesAllowed', 'yes');
toastObject.goAway(0);
}
}
]
});
}
})();
}
}

View File

@@ -1,8 +1,8 @@
<template>
<div class="page page-error">
<h1>{{ error.statusCode }}</h1>
<h2>{{ error.message }}</h2>
<br>
<img src="~static/icons/error.svg" alt="Error" class="error_banner">
<h2>{{ error.statusCode }}</h2>
<h3>{{ error.message }}</h3>
<p><nuxt-link to="/"><button>Go Home</button></nuxt-link></p>
<p><a href="" @click.prevent="reloadApplication">Reload</a></p>
</div>
@@ -11,14 +11,16 @@
<style lang="scss">
// Center the error page in the viewport.
.page-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}
.error_banner {
width: 256px;
}
</style>
<script>

View File

@@ -187,6 +187,14 @@ export default {
loading: {
color: 'var(--ac-color)'
},
/*
** Customize the loading indicator
*/
loadingIndicator: {
name: 'pulse',
color: 'var(--ac-color)',
background: 'var(--bg-color)'
},
/*
** Global CSS
*/
@@ -216,25 +224,27 @@ export default {
['@nuxtjs/pwa'],
['@nuxtjs/axios'],
['@nuxtjs/toast'],
['@nuxtjs/google-analytics']
['@nuxtjs/google-analytics'],
['@nuxtjs/sitemap'],
['@nuxtjs/google-tag-manager', { id: process.env.GTM_ID || 'GTM-MXWD8NQ' }]
],
pwa: {
manifest: {
name: meta.name,
short_name: meta.name,
display: "standalone",
theme_color: "#252628",
background_color: "#252628",
start_url: `${routerBase.router.base}`
},
meta: {
description: meta.shortDescription,
theme_color: "#252628",
},
icons: ((sizes) => {
let icons = [];
for (let size of sizes) {
@@ -253,7 +263,10 @@ export default {
theme: 'bubble'
},
googleAnalytics: {
id: 'UA-61422507-2'
id: process.env.GA_ID || 'UA-61422507-2'
},
sitemap: {
hostname: 'https://postwoman.io'
},
/*
** Build configuration

2488
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,22 +11,30 @@
"build": "nuxt build",
"start": "nuxt start",
"pregenerate": "node build.js",
"generate": "nuxt generate"
"generate": "nuxt generate",
"e2e": "cypress run",
"e2e:open": "cypress open",
"dev:e2e": "start-server-and-test dev http://localhost:3000 e2e:open",
"test": "start-server-and-test dev http://localhost:3000 e2e"
},
"dependencies": {
"@nuxtjs/axios": "^5.6.0",
"@nuxtjs/google-analytics": "^2.2.0",
"@nuxtjs/google-tag-manager": "^2.3.0",
"@nuxtjs/pwa": "^3.0.0-beta.19",
"@nuxtjs/sitemap": "^2.0.0",
"@nuxtjs/toast": "^3.2.1",
"highlight.js": "^9.15.10",
"nuxt": "^2.9.2",
"nuxt": "^2.10.1",
"vue-virtual-scroll-list": "^1.4.2",
"vuejs-auto-complete": "^0.9.0",
"vuex-persist": "^2.1.0",
"yargs-parser": "^14.0.0"
"yargs-parser": "^15.0.0"
},
"devDependencies": {
"cypress": "^3.4.1",
"node-sass": "^4.12.0",
"sass-loader": "^7.3.1"
"sass-loader": "^7.3.1",
"start-server-and-test": "^1.10.5"
}
}

View File

@@ -82,8 +82,11 @@
<ul>
<li>
<label for="contentType">Content Type</label>
<autocomplete :source="validContentTypes" :spellcheck="false" v-model="contentType">Content Type
</autocomplete>
<autocomplete :source="validContentTypes" :spellcheck="false" v-model="contentType">Content Type</autocomplete>
</li>
</ul>
<ul>
<li>
<span>
<pw-toggle :on="rawInput" @change="rawInput = !rawInput">
Raw Input {{ rawInput ? "Enabled" : "Disabled" }}
@@ -131,6 +134,11 @@
</ul>
</div>
</div>
<ul>
<li>
<input id="label" name="label" type="text" v-model="label" placeholder="Label request">
</li>
</ul>
<div class="flex-wrap">
<button class="icon" id="show-modal" @click="showModal = true">
<i class="material-icons">import_export</i>
@@ -419,6 +427,7 @@
},
data() {
return {
label: '',
showModal: false,
copyButton: '<i class="material-icons">file_copy</i>',
copiedButton: '<i class="material-icons">done</i>',
@@ -530,6 +539,9 @@
selectedRequest() {
return this.$store.state.postwoman.selectedRequest;
},
requestName() {
return this.label
},
statusCategory() {
return findStatusGroup(this.response.status);
},
@@ -603,7 +615,7 @@
requestString.push('const xhr = new XMLHttpRequest()');
const user = this.auth === 'Basic' ? this.httpUser : null
const pswd = this.auth === 'Basic' ? this.httpPassword : null
requestString.push('xhr.open(' + this.method + ', ' + this.url + this.path + this.queryString + ', true, ' +
requestString.push('xhr.open("' + this.method + '", "' + this.url + this.path + this.queryString + '", true, ' +
user + ', ' + pswd + ')');
if (this.auth === 'Bearer Token') {
requestString.push("xhr.setRequestHeader('Authorization', 'Bearer ' + " + this.bearerToken + ")");
@@ -625,7 +637,7 @@
} else if (this.requestType == 'Fetch') {
var requestString = [];
var headers = [];
requestString.push('fetch(' + this.url + this.path + this.queryString + ', {\n')
requestString.push('fetch("' + this.url + this.path + this.queryString + '", {\n')
requestString.push(' method: "' + this.method + '",\n')
if (this.auth === 'Basic') {
var basic = this.httpUser + ':' + this.httpPassword;
@@ -685,10 +697,12 @@
},
methods: {
handleUseHistory({
label,
method,
url,
path
}) {
this.label = label;
this.method = method;
this.url = url;
this.path = path;
@@ -763,6 +777,7 @@
headers = headersObject;
try {
const startTime = Date.now();
const payload = await this.$axios({
method: this.method,
url: this.url + this.pathName + this.queryString,
@@ -771,6 +786,11 @@
data: requestBody ? requestBody.toString() : null
});
const duration = Date.now() - startTime;
this.$toast.info(`Finished in ${duration}ms`, {
icon: 'done'
});
(() => {
const status = this.response.status = payload.status;
const headers = this.response.headers = payload.headers;
@@ -783,6 +803,7 @@
// Addition of an entry to the history component.
const entry = {
label: this.requestName,
status,
date,
time,
@@ -800,6 +821,7 @@
// 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(),
@@ -1000,9 +1022,9 @@
sendButtonElement.classList.toggle('show');
});
}, {
threshold: 1
rootMargin: '0px',
threshold: [0],
});
observer.observe(requestElement);
},
handleImport() {
@@ -1053,6 +1075,7 @@
this.params = [];
break;
default:
this.label = '',
this.method= 'GET',
this.url = 'https://reqres.in',
this.auth = 'None',
@@ -1076,6 +1099,7 @@
created() {
if (Object.keys(this.$route.query).length) this.setRouteQueries(this.$route.query);
this.$watch(vm => [
vm.label,
vm.method,
vm.url,
vm.auth,

View File

@@ -78,16 +78,28 @@
// NOTE:: You need to first set the CSS for your theme in /assets/css/themes.scss
// You should copy the existing light theme as a template and then just
// set the relevant values.
themes: [{
themes: [
{
"color": "rgb(37, 38, 40)",
"name": "Dark (default)",
"name": "Kinda Dark",
"class": ""
},
{
"color": "#ebeef5",
"name": "Light",
"color": "#ffffff",
"name": "Clearly White",
"vibrant": true,
"class": "light"
},
{
"color": "#000000",
"name": "Just Black",
"class": "black"
},
{
"color": "var(--bg-color)",
"name": "Auto (system)",
"vibrant": window.matchMedia('(prefers-color-scheme: light)').matches,
"class": "auto"
}
],
// You can define a new color here! It will simply store the color value.
@@ -95,7 +107,7 @@
// If the color is vibrant, black is used as the active foreground color.
{
"color": "#50fa7b",
"name": "Green (default)",
"name": "Green",
"vibrant": true
},
{

View File

@@ -62,7 +62,7 @@
&,
span {
font-size: 18px;
font-family: monospace;
font-family: 'Roboto Mono', monospace;
}
span {

1
static/icons/error.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

7
storage.rules Normal file
View File

@@ -0,0 +1,7 @@
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth!=null;
}
}
}

View File

@@ -0,0 +1 @@
{ "message": "FAKE Cat API" }

View File

@@ -0,0 +1,7 @@
describe('Visit home', () => {
it('Have a page title with "Postwoman"', () => {
cy.visit('/')
.get('title')
.should('contain','Postwoman')
})
})

View File

@@ -0,0 +1,57 @@
describe('Methods', () => {
const methods = [ 'POST', 'HEAD', 'POST', 'PUT', 'DELETE','OPTIONS', 'PATCH']
methods.forEach(method => {
it(`Change the default method GET to ${method} with url query`, () => {
cy.visit(`/?method=${method}`)
.get('#method').contains(method)
})
})
})
describe('Url and path', () => {
it('Change default url with query and reset default path to empty string and make a request to cat api', () => {
cy.seedAndVisit('catapi', '/?url=https://api.thecatapi.com&path=')
.get('#url').then(el => expect(el.val() === 'https://api.thecatapi.com').to.equal(true))
.get("#path").then(el => expect(el.val() === '').to.equal(true))
.get('#response-details-wrapper').should($wrapper => {
expect($wrapper).to.contain('FAKE Cat API')
})
})
})
describe('Authentication', () => {
it(`Change default auth 'None' to 'Basic' and set httpUser and httpPassword with url query`, () => {
cy.visit(`?&auth=Basic&httpUser=foo&httpPassword=bar`, { retryOnStatusCodeFailure: true })
.get('#authentication').contains('Authentication').click()
.then(() => {
cy.get('input[name="http_basic_user"]', { timeout: 500 })
.invoke('val')
.then(user => {
expect(user === 'foo').to.equal(true)
cy.log('Success! user === foo')
})
cy.get('input[name="http_basic_passwd"]')
.invoke('val')
.then(user => {
expect(user === 'bar').to.equal(true)
cy.log('Success! password === bar')
})
})
})
const base64Tkn = encodeURI(btoa('{"alg":"HS256", "typ": "JWT"}'))
it(`Change default auth 'None' to 'Bearer token' and set bearerToken with url query`, () => {
cy.visit(`/?auth=Bearer Token&bearerToken=${base64Tkn}`, { retryOnStatusCodeFailure: true })
.get('#authentication').contains('Authentication').click()
.then(() => {
cy.get('input[name="bearer_token"]', { timeout: 500 })
.invoke('val')
.then(tkn => {
expect(tkn === base64Tkn).to.equal(true)
cy.log(`Success! input[name="bearer_token"] === ${base64Tkn}`)
})
})
})
})

View File

@@ -0,0 +1,16 @@
/**
* Creates cy.seedAndVisit() function
* This function will go to some path and wait for some fake response from 'src/tests/fixtures/*.json'
* @param { String } seedData The name of json at 'src/tests/fixtures/
* @param { String } path The path or query parameters to go -ex. '/?path=/api/users'
* @param { String } method The fake request method
*/
Cypress.Commands.add('seedAndVisit', (seedData, path = '/', method = 'GET') => {
cy.server()
.route(method, 'https://api.thecatapi.com/', `fixture:${seedData}`).as(
'load'
)
cy.visit(path)
.get('#send').click()
.wait('@load')
})

View File

@@ -0,0 +1 @@
import './commands'