even merge (#528)

even merge
This commit is contained in:
Liyas Thomas
2020-01-27 05:36:22 +05:30
committed by GitHub
33 changed files with 2533 additions and 307 deletions

View File

@@ -13,38 +13,40 @@
language: node_js
node_js:
- "12"
- "12"
os: linux
addons:
apt:
packages:
- libgconf-2-4 # cypress binary dependency
apt:
packages:
- libgconf-2-4 # cypress binary dependency
env:
- DEPLOY_ENV=POSTWOMAN_IO
- DEPLOY_ENV=POSTWOMAN_IO
cache:
npm: true
directories:
- "node_modules"
- ~/.cache
npm: true
directories:
- "node_modules"
- ~/.cache
branches:
only:
- "master"
only:
- "master"
install:
- "npm install firebase-tools"
- "npm install"
- "npm install firebase-tools"
- "npm install"
before_script:
- "npm run test"
- "npm run test"
script:
- "cd functions"
- "npm install"
- "cd .."
- "npm run generate"
- "cd functions"
- "npm install"
- "cd .."
- "npm run generate"
notifications:
webhooks: https://www.travisbuddy.com

View File

@@ -3,10 +3,10 @@
<br>
<br>
<p>
API request builder - A free, fast, and beautiful alternative to Postman
<b>A free, fast & beautiful API request builder</b>
</p>
<p>
Helps you create your requests faster, saving you precious time on your development - <a href="https://postwoman.launchaco.com">Subscribe</a>
<i>Web alternative to Postman - Helps you create requests faster, saving precious time on development - <a href="https://postwoman.launchaco.com">Subscribe</a></i>
</p>
<p>
@@ -69,8 +69,8 @@ _Customized themes are synced with local session storage_
- 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)
- Add to Home Screen
- Desktop PWA
- ([full features](https://developers.google.com/web/progressive-web-apps))
🚀 **Request**: Retrieve response from endpoint instantly.
@@ -190,38 +190,36 @@ _Requests with Pre-Request Scripts are indicated in History entries_
🌎 **i18n β**: Experience the app in your own language.
1. Scroll down to the footer
2. Click "Choose Language" button
2. Click "Choose Language" icon button
3. Select your language from the menu
_Keep in mind translations aren't available for all source and target language combinations_
_Keep in mind: Translations aren't available for all source and target language combinations_
**To provide a localized experience for users around the world, you can add you own translations.**
- Add a new locale in `lang/`
Example: `lang/es-ES.js`
- Mention `code`, `name`, `iso` and `file` in `nuxt.config.js`
Example:
```
i18n: {
locales: [{
code: 'es',
name: 'Español',
iso: 'es-ES',
file: 'es-ES.js'
}]
}
```
_**All `i18n` contributions are welcome to `i18n` [branch](https://github.com/liyasthomas/postwoman/tree/i18n) only!**_
📦 **Add-ons**: Official add-ons for Postwoman.
- **[Postwoman Proxy β](https://github.com/postwoman-io/postwoman-proxy)** - A simple proxy server created for Postwoman
- **[Postwoman CLI β](https://github.com/postwoman-io/postwoman-cli)** - A CLI solution for Postwoman
- **[Proxy β](https://github.com/postwoman-io/postwoman-proxy)** - A simple proxy server created for Postwoman
- **[CLI β](https://github.com/postwoman-io/postwoman-cli)** - A CLI solution for Postwoman
- **Browser Extensions** - Browser extensions that simplifies access to Postwoman
_Add-ons are developed and maintained under **[Official Postwoman Organization](https://github.com/postwoman-io)**_
[![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_16x16.png) **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/postwoman) ([GitHub](https://github.com/AndrewBastin/postwoman-firefox)) &nbsp;|&nbsp; [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_16x16.png) **Chrome**](https://chrome.google.com/webstore/detail/postwoman-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld) ([GitHub](https://github.com/AndrewBastin/postwoman-chrome))
>**Extensions fixes `CORS` issues.**
_Add-ons are developed and maintained under **[Official Postwoman Organization](https://github.com/postwoman-io)**._
☁️ **Auth + Sync**: Sign in and sync in real-time.
**Sign in with:**
- Google
- GitHub
**Sync:**
- History
- Collections
**To find out more, please check out [Postwoman Wiki](https://github.com/liyasthomas/postwoman/wiki).**

View File

@@ -27,6 +27,7 @@
-webkit-font-feature-settings: "liga";
-webkit-font-smoothing: antialiased;
font-feature-settings: "liga";
border-radius: 50%;
}
/* poppins-500 - latin */

View File

@@ -592,7 +592,6 @@ pre {
select {
height: 37px;
background-color: var(--bg-dark-color);
cursor: pointer;
-webkit-appearance: none;
-moz-appearance: none;
@@ -698,6 +697,7 @@ ol li {
.show-on-large-screen {
display: inline-flex;
flex: 1;
}
.info-response {
@@ -852,10 +852,7 @@ input[type="radio"]:checked + label + .tab {
}
@media (max-width: $responsiveWidth) {
.content {
flex-flow: column;
}
.content,
.columns {
flex-flow: column;
}

View File

@@ -53,7 +53,7 @@ try {
// Write version data into a file
fs.writeFileSync(
PW_BUILD_DATA_DIR + "/version.json",
`${PW_BUILD_DATA_DIR}/version.json`,
JSON.stringify(version)
);
})();

View File

@@ -17,7 +17,11 @@
<div slot="body">
<ul>
<li>
<input type="text" v-model="name" :placeholder="$t('my_new_collection')" />
<input
type="text"
v-model="name"
:placeholder="$t('my_new_collection')"
/>
</li>
</ul>
</div>
@@ -52,6 +56,10 @@ export default {
},
methods: {
addNewCollection() {
if (!this.$data.name) {
this.$toast.info("Please provide a valid name for the collection");
return;
}
this.$store.commit("postwoman/addNewCollection", {
name: this.$data.name
});

View File

@@ -17,7 +17,11 @@
<div slot="body">
<ul>
<li>
<input type="text" v-model="name" :placeholder="$t('my_new_folder')" />
<input
type="text"
v-model="name"
:placeholder="$t('my_new_folder')"
/>
</li>
</ul>
</div>

View File

@@ -59,6 +59,10 @@ export default {
},
methods: {
saveCollection() {
if (!this.$data.name) {
this.$toast.info("Please provide a valid name for the collection");
return;
}
const collectionUpdated = {
...this.$props.editingCollection,
name: this.$data.name

View File

@@ -11,6 +11,54 @@
</button>
</div>
</div>
<div class="flex-wrap">
<span
v-tooltip="{
content: !fb.currentUser
? $t('login_first')
: $t('replace_current')
}"
>
<button
:disabled="!fb.currentUser"
class="icon"
@click="syncCollections"
>
<i class="material-icons">folder_shared</i>
<span>{{ $t("import_from_sync") }}</span>
</button>
</span>
<button
class="icon"
@click="openDialogChooseFileToReplaceWith"
v-tooltip="$t('replace_current')"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("replace_json") }}</span>
<input
type="file"
@change="replaceWithJSON"
style="display: none;"
ref="inputChooseFileToReplaceWith"
accept="application/json"
/>
</button>
<button
class="icon"
@click="openDialogChooseFileToImportFrom"
v-tooltip="$t('preserve_current')"
>
<i class="material-icons">folder_special</i>
<span>{{ $t("import_json") }}</span>
<input
type="file"
@change="importFromJSON"
style="display: none;"
ref="inputChooseFileToImportFrom"
accept="application/json"
/>
</button>
</div>
</li>
</ul>
</div>
@@ -18,39 +66,6 @@
<textarea v-model="collectionJson" rows="8"></textarea>
</div>
<div slot="footer">
<div class="flex-wrap">
<span>
<button
class="icon"
@click="openDialogChooseFileToReplaceWith"
v-tooltip="$t('replace_current')"
>
<i class="material-icons">create_new_folder</i>
<span>{{ $t("replace_json") }}</span>
<input
type="file"
@change="replaceWithJSON"
style="display: none;"
ref="inputChooseFileToReplaceWith"
/>
</button>
<button
class="icon"
@click="openDialogChooseFileToImportFrom"
v-tooltip="$t('preserve_current')"
>
<i class="material-icons">folder_shared</i>
<span>{{ $t("import_json") }}</span>
<input
type="file"
@change="importFromJSON"
style="display: none;"
ref="inputChooseFileToImportFrom"
/>
</button>
</span>
<span></span>
</div>
<div class="flex-wrap">
<span></span>
<span>
@@ -71,7 +86,14 @@
</template>
<script>
import { fb } from "../../functions/fb";
export default {
data() {
return {
fb
};
},
props: {
show: Boolean
},
@@ -101,6 +123,7 @@ export default {
this.$store.commit("postwoman/replaceCollections", collections);
};
reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0]);
this.fileImported();
},
importFromJSON() {
let reader = new FileReader();
@@ -110,6 +133,7 @@ export default {
this.$store.commit("postwoman/importCollections", collections);
};
reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0]);
this.fileImported();
},
exportJSON() {
let text = this.collectionJson;
@@ -125,6 +149,18 @@ export default {
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
this.$toast.success(this.$t("download_started"), {
icon: "done"
});
},
syncCollections() {
this.$store.commit("postwoman/replaceCollections", fb.currentCollections);
this.fileImported();
},
fileImported() {
this.$toast.info(this.$t("file_imported"), {
icon: "folder_shared"
});
}
}
};

View File

@@ -47,12 +47,8 @@ TODO:
</button>
</div>
<div>
<button
class="icon"
@click="displayModalImportExport(true)"
v-tooltip="$t('import_export')"
>
<i class="material-icons">import_export</i>
<button class="icon" @click="displayModalImportExport(true)">
{{ $t("import_export") }}
</button>
<!-- <a
href="https://github.com/liyasthomas/postwoman/wiki/Collections"
@@ -90,12 +86,18 @@ TODO:
</li>
</ul>
</virtual-list>
<nuxt-link :to="localePath('doc')" :aria-label="$t('documentation')">
<button class="icon">
<i class="material-icons">books</i>
<span>{{ $t("generate_docs") }}</span>
</button>
</nuxt-link>
</div>
</template>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 232px);
max-height: calc(100vh - 276px);
}
ul {
@@ -106,6 +108,7 @@ ul {
<script>
import collection from "./collection";
import { fb } from "../../functions/fb";
export default {
components: {
@@ -170,11 +173,13 @@ export default {
this.$data.editingCollection = collection;
this.$data.editingCollectionIndex = collectionIndex;
this.displayModalEdit(true);
this.syncCollections();
},
addFolder(collection, collectionIndex) {
this.$data.editingCollection = collection;
this.$data.editingCollectionIndex = collectionIndex;
this.displayModalAddFolder(true);
this.syncCollections();
},
editFolder(payload) {
const { collectionIndex, folder, folderIndex } = payload;
@@ -183,6 +188,7 @@ export default {
this.$data.editingFolder = folder;
this.$data.editingFolderIndex = folderIndex;
this.displayModalEditFolder(true);
this.syncCollections();
},
editRequest(payload) {
const { request, collectionIndex, folderIndex, requestIndex } = payload;
@@ -191,6 +197,7 @@ export default {
this.$data.editingRequest = request;
this.$data.editingRequestIndex = requestIndex;
this.displayModalEditRequest(true);
this.syncCollections();
},
resetSelectedData() {
this.$data.editingCollection = undefined;
@@ -199,6 +206,15 @@ export default {
this.$data.editingFolderIndex = undefined;
this.$data.editingRequest = undefined;
this.$data.editingRequestIndex = undefined;
},
syncCollections() {
if (fb.currentUser !== null) {
if (fb.currentSettings[0].value) {
fb.writeCollections(
JSON.parse(JSON.stringify(this.$store.state.postwoman.collections))
);
}
}
}
}
};

View File

@@ -1,7 +1,11 @@
<template>
<div class="flex-wrap">
<div>
<button class="icon" @click="selectRequest()" v-tooltip="$t('use_request')">
<button
class="icon"
@click="selectRequest()"
v-tooltip="$t('use_request')"
>
<i class="material-icons">insert_drive_file</i>
<span>{{ request.name }}</span>
</button>

View File

@@ -0,0 +1,85 @@
<template>
<virtual-list
v-if="fb.currentFeeds.length !== 0"
class="virtual-list"
:class="{ filled: fb.currentFeeds.length }"
:size="56"
:remain="Math.min(8, fb.currentFeeds.length)"
>
<ul v-for="feed in fb.currentFeeds" :key="feed.id">
<div class="show-on-large-screen">
<li>
<input
:aria-label="$t('label')"
type="text"
readonly
:value="feed.label"
:placeholder="$t('no_label')"
class="bg-color"
/>
</li>
<button class="icon" @click="saveFeed(feed)">
<i class="material-icons">get_app</i>
</button>
<button class="icon" @click="deleteFeed(feed)">
<i class="material-icons">delete</i>
</button>
</div>
</ul>
</virtual-list>
<ul v-else>
<li>
<label class="info">{{ $t("empty") }}</label>
</li>
</ul>
</template>
<style scoped lang="scss">
.virtual-list {
max-height: calc(100vh - 288px);
}
.bg-color {
background-color: transparent;
}
</style>
<script>
import { fb } from "../../functions/fb";
export default {
components: {
VirtualList: () => import("vue-virtual-scroll-list")
},
data() {
return {
fb
};
},
methods: {
deleteFeed(feed) {
fb.deleteFeed(feed.id);
this.$toast.error(this.$t("deleted"), {
icon: "delete"
});
},
saveFeed(feed) {
const dataToWrite = JSON.stringify(feed.message, null, 2);
const file = new Blob([dataToWrite], { type: "application/json" });
const a = document.createElement("a"),
url = URL.createObjectURL(file);
a.href = url;
a.download = (feed.label + " on " + Date()).replace(/\./g, "[dot]");
document.body.appendChild(a);
a.click();
this.$toast.success(this.$t("download_started"), {
icon: "done"
});
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 1000);
}
}
};
</script>

View File

@@ -0,0 +1,55 @@
<template>
<div>
<ul>
<li>
<input
:aria-label="$t('label')"
type="text"
autofocus
v-model="message"
:placeholder="$t('paste_a_collection')"
/>
</li>
</ul>
<ul>
<li>
<input
:aria-label="$t('label')"
type="text"
autofocus
v-model="label"
:placeholder="$t('label')"
/>
</li>
<button
class="icon"
:disabled="!(this.message && this.label)"
value="Save"
@click="formPost"
>
<i class="material-icons">add</i>
<span>Add</span>
</button>
</ul>
</div>
</template>
<script>
import { fb } from "../../functions/fb";
export default {
data() {
return {
message: null,
label: null
};
},
methods: {
formPost() {
fb.writeFeeds(this.message, this.label);
this.message = null;
this.label = null;
}
}
};
</script>

View File

@@ -0,0 +1,115 @@
<template>
<v-popover>
<button class="icon" v-tooltip="$t('login_with')">
<i class="material-icons">vpn_key</i>
</button>
<template slot="popover">
<div>
<button class="icon" @click="signInWithGoogle" v-close-popover>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="material-icons"
>
<path
d="M12.24 10.285V14.4h6.806c-.275 1.765-2.056 5.174-6.806 5.174-4.095 0-7.439-3.389-7.439-7.574s3.345-7.574 7.439-7.574c2.33 0 3.891.989 4.785 1.849l3.254-3.138C18.189 1.186 15.479 0 12.24 0c-6.635 0-12 5.365-12 12s5.365 12 12 12c6.926 0 11.52-4.869 11.52-11.726 0-.788-.085-1.39-.189-1.989H12.24z"
/>
</svg>
<span>Google</span>
</button>
</div>
<div>
<button class="icon" @click="signInWithGithub" v-close-popover>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="material-icons"
>
<path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
</svg>
<span>GitHub</span>
</button>
</div>
</template>
</v-popover>
</template>
<script>
import firebase from "firebase/app";
import { fb } from "../../functions/fb";
export default {
data() {
return {
fb
};
},
methods: {
signInWithGoogle() {
const provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithPopup(provider)
.then(res => {
if (res.additionalUserInfo.isNewUser) {
this.$toast.info(this.$t("turn_on") + " " + this.$t("sync"), {
icon: "sync",
duration: null,
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: (e, toastObject) => {
fb.writeSettings("syncHistory", false);
fb.writeSettings("syncCollections", true);
this.$router.push({ path: "/settings" });
toastObject.remove();
}
}
});
}
})
.catch(err => {
this.$toast.show(err.message || err, {
icon: "error"
});
});
},
signInWithGithub() {
const provider = new firebase.auth.GithubAuthProvider();
firebase
.auth()
.signInWithPopup(provider)
.then(res => {
if (res.additionalUserInfo.isNewUser) {
this.$toast.info(this.$t("turn_on") + " " + this.$t("sync"), {
icon: "sync",
duration: null,
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: (e, toastObject) => {
fb.writeSettings("syncHistory", false);
fb.writeSettings("syncCollections", true);
this.$router.push({ path: "/settings" });
toastObject.remove();
}
}
});
}
})
.catch(err => {
this.$toast.show(err.message || err, {
icon: "error"
});
});
}
}
};
</script>

View File

@@ -0,0 +1,135 @@
<template>
<pre ref="editor"></pre>
</template>
<script>
const DEFAULT_THEME = "twilight";
import ace from "ace-builds";
import * as gql from "graphql";
import "ace-builds/webpack-resolver";
import debounce from "../../functions/utils/debounce";
export default {
props: {
value: {
type: String,
default: ""
},
theme: {
type: String,
required: false
},
lang: {
type: String,
default: "json"
},
options: {
type: Object,
default: {}
}
},
data() {
return {
editor: null,
cacheValue: "",
validationSchema: null
};
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1);
this.cacheValue = value;
}
},
theme() {
this.editor.setTheme("ace/theme/" + this.defineTheme());
},
lang(value) {
this.editor.getSession().setMode("ace/mode/" + value);
},
options(value) {
this.editor.setOptions(value);
}
},
mounted() {
const editor = ace.edit(this.$refs.editor, {
theme: "ace/theme/" + this.defineTheme(),
mode: "ace/mode/" + this.lang,
...this.options
});
if (this.value) editor.setValue(this.value, 1);
this.editor = editor;
this.cacheValue = this.value;
editor.on("change", () => {
const content = editor.getValue();
this.$emit("input", content);
this.parseContents(content);
this.cacheValue = content;
});
this.parseContents(this.value);
},
methods: {
defineTheme() {
if (this.theme) {
return this.theme;
} else {
return (
this.$store.state.postwoman.settings.THEME_ACE_EDITOR || DEFAULT_THEME
);
}
},
setValidationSchema(schema) {
this.validationSchema = schema;
this.parseContents(this.cacheValue);
},
parseContents: debounce(function(content) {
if (content !== "") {
try {
const doc = gql.parse(content);
if (this.validationSchema) {
this.editor.session.setAnnotations(
gql.validate(this.validationSchema, doc).map(err => {
return {
row: err.locations[0].line - 1,
column: err.locations[0].column - 1,
text: err.message,
type: "error"
};
})
);
}
} catch (e) {
this.editor.session.setAnnotations([
{
row: e.locations[0].line - 1,
column: e.locations[0].column - 1,
text: e.message,
type: "error"
}
]);
}
} else {
this.editor.session.setAnnotations([]);
}
}, 2000)
},
beforeDestroy() {
this.editor.destroy();
this.editor.container.remove();
}
};
</script>

View File

@@ -1,10 +1,5 @@
<template>
<pw-section
class="green"
icon="history"
:label="$t('history')"
ref="history"
>
<pw-section class="green" icon="history" :label="$t('history')" ref="history">
<ul>
<li id="filter-history">
<input
@@ -26,8 +21,10 @@
<button
class="icon"
:class="{ stared: entry.star }"
@click="toggleStar(index)"
v-tooltip="{ content: !entry.star ? $t('add_star') : $t('remove_star') }"
@click="toggleStar(entry)"
v-tooltip="{
content: !entry.star ? $t('add_star') : $t('remove_star')
}"
>
<i class="material-icons">
{{ entry.star ? "star" : "star_border" }}
@@ -238,7 +235,9 @@
</v-popover>
</div>
<div class="flex-wrap" v-else>
<label for="clear-history-button" class="info">{{ $t("are_you_sure") }}</label>
<label for="clear-history-button" class="info">
{{ $t("are_you_sure") }}
</label>
<div>
<button
class="icon"
@@ -332,6 +331,7 @@ ol li {
<script>
import { findStatusGroup } from "../pages/index";
import { fb } from "../functions/fb";
const updateOnLocalStorage = (propertyName, property) =>
window.localStorage.setItem(propertyName, JSON.stringify(property));
@@ -342,11 +342,11 @@ export default {
VirtualList: () => import("vue-virtual-scroll-list")
},
data() {
const localStorageHistory = JSON.parse(
window.localStorage.getItem("history")
);
return {
history: localStorageHistory || [],
history:
fb.currentUser !== null
? fb.currentHistory
: JSON.parse(window.localStorage.getItem("history")) || [],
filterText: "",
showFilter: false,
isClearingHistory: false,
@@ -360,6 +360,10 @@ export default {
},
computed: {
filteredHistory() {
this.history =
fb.currentUser !== null
? fb.currentHistory
: JSON.parse(window.localStorage.getItem("history")) || [];
return this.history.filter(entry => {
const filterText = this.filterText.toLowerCase();
return Object.keys(entry).some(key => {
@@ -372,6 +376,9 @@ export default {
},
methods: {
clearHistory() {
if (fb.currentUser !== null) {
fb.clearHistory();
}
this.history = [];
this.filterText = "";
this.disableHistoryClearing();
@@ -392,6 +399,9 @@ export default {
);
},
deleteHistory(entry) {
if (fb.currentUser !== null) {
fb.deleteHistory(entry);
}
this.history.splice(this.history.indexOf(entry), 1);
if (this.history.length === 0) {
this.filterText = "";
@@ -498,8 +508,11 @@ export default {
toggleCollapse() {
this.showMore = !this.showMore;
},
toggleStar(index) {
this.history[index]["star"] = !this.history[index]["star"];
toggleStar(entry) {
if (fb.currentUser !== null) {
fb.toggleStar(entry, !entry.star);
}
entry.star = !entry.star;
updateOnLocalStorage("history", this.history);
}
}

View File

@@ -1,7 +1,14 @@
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
allow read, write: if request.auth.uid != null;
}
// Make sure the uid of the requesting user matches name of the user
// document. The wildcard expression {userId} makes the userId variable
// available in rules.
match /users/{userId} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
}
}

192
functions/fb.js Normal file
View File

@@ -0,0 +1,192 @@
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/auth";
// Initialize Firebase, copied from cloud console
const firebaseConfig = {
apiKey: "AIzaSyCMsFreESs58-hRxTtiqQrIcimh4i1wbsM",
authDomain: "postwoman-api.firebaseapp.com",
databaseURL: "https://postwoman-api.firebaseio.com",
projectId: "postwoman-api",
storageBucket: "postwoman-api.appspot.com",
messagingSenderId: "421993993223",
appId: "1:421993993223:web:ec0baa8ee8c02ffa1fc6a2",
measurementId: "G-ERJ6025CEB"
};
firebase.initializeApp(firebaseConfig);
// a reference to the users collection
const usersCollection = firebase.firestore().collection("users");
// the shared state object that any vue component
// can get access to
export const fb = {
currentUser: {},
currentFeeds: [],
currentSettings: [],
currentHistory: [],
currentCollections: [],
writeFeeds: async (message, label) => {
const dt = {
createdOn: new Date(),
author: fb.currentUser.uid,
author_name: fb.currentUser.displayName,
author_image: fb.currentUser.photoURL,
message,
label
};
usersCollection
.doc(fb.currentUser.uid)
.collection("feeds")
.add(dt)
.catch(e => console.error("error inserting", dt, e));
},
deleteFeed: id => {
usersCollection
.doc(fb.currentUser.uid)
.collection("feeds")
.doc(id)
.delete()
.catch(e => console.error("error deleting", id, e));
},
writeSettings: async (setting, value) => {
const st = {
updatedOn: new Date(),
author: fb.currentUser.uid,
author_name: fb.currentUser.displayName,
author_image: fb.currentUser.photoURL,
name: setting,
value
};
usersCollection
.doc(fb.currentUser.uid)
.collection("settings")
.doc(setting)
.set(st)
.catch(e => console.error("error updating", st, e));
},
writeHistory: async entry => {
const hs = entry;
usersCollection
.doc(fb.currentUser.uid)
.collection("history")
.add(hs)
.catch(e => console.error("error inserting", hs, e));
},
deleteHistory: entry => {
usersCollection
.doc(fb.currentUser.uid)
.collection("history")
.doc(entry.id)
.delete()
.catch(e => console.error("error deleting", entry, e));
},
clearHistory: () => {
usersCollection
.doc(fb.currentUser.uid)
.collection("history")
.get()
.then(({ docs }) => {
docs.forEach(e => fb.deleteHistory(e));
});
},
toggleStar: (entry, value) => {
usersCollection
.doc(fb.currentUser.uid)
.collection("history")
.doc(entry.id)
.update({ star: value })
.catch(e => console.error("error deleting", entry, e));
},
writeCollections: async collection => {
const cl = {
updatedOn: new Date(),
author: fb.currentUser.uid,
author_name: fb.currentUser.displayName,
author_image: fb.currentUser.photoURL,
collection: collection
};
usersCollection
.doc(fb.currentUser.uid)
.collection("collections")
.doc("sync")
.set(cl)
.catch(e => console.error("error updating", cl, e));
}
};
// When a user logs in or out, save that in the store
firebase.auth().onAuthStateChanged(user => {
if (user) {
fb.currentUser = user;
fb.currentUser.providerData.forEach(profile => {
let us = {
updatedOn: new Date(),
provider: profile.providerId,
name: profile.displayName,
email: profile.email,
photoUrl: profile.photoURL,
uid: profile.uid
};
usersCollection
.doc(fb.currentUser.uid)
.set(us)
.catch(e => console.error("error updating", us, e));
});
usersCollection
.doc(fb.currentUser.uid)
.collection("feeds")
.orderBy("createdOn", "desc")
.onSnapshot(feedsRef => {
const feeds = [];
feedsRef.forEach(doc => {
const feed = doc.data();
feed.id = doc.id;
feeds.push(feed);
});
fb.currentFeeds = feeds;
});
usersCollection
.doc(fb.currentUser.uid)
.collection("settings")
.onSnapshot(settingsRef => {
const settings = [];
settingsRef.forEach(doc => {
const setting = doc.data();
setting.id = doc.id;
settings.push(setting);
});
fb.currentSettings = settings;
});
usersCollection
.doc(fb.currentUser.uid)
.collection("history")
.onSnapshot(historyRef => {
const history = [];
historyRef.forEach(doc => {
const entry = doc.data();
entry.id = doc.id;
history.push(entry);
});
fb.currentHistory = history;
});
usersCollection
.doc(fb.currentUser.uid)
.collection("collections")
.onSnapshot(collectionsRef => {
const collections = [];
collectionsRef.forEach(doc => {
const collection = doc.data();
collection.id = doc.id;
collections.push(collection);
});
fb.currentCollections = collections[0].collection;
});
} else {
fb.currentUser = null;
}
});

24
functions/network.js Normal file
View File

@@ -0,0 +1,24 @@
import AxiosStrategy from "./strategies/AxiosStrategy";
import FirefoxStrategy from "./strategies/FirefoxStrategy";
import ChromeStrategy, { hasChromeExtensionInstalled } from "./strategies/ChromeStrategy";
const runAppropriateStrategy = (req, store) => {
// Chrome Provides a chrome object for scripts to access
// Check its availability to say whether you are in Google Chrome
if (window.chrome && hasChromeExtensionInstalled()) {
return ChromeStrategy(req, store);
}
// The firefox plugin injects a function to send requests through it
// If that is available, then we can use the FirefoxStrategy
if (window.firefoxExtSendRequest) {
return FirefoxStrategy(req, store);
}
return AxiosStrategy(req, store);
}
const sendNetworkRequest = (req, store) =>
runAppropriateStrategy(req, store)
.finally(() => window.$nuxt.$loading.finish());
export { sendNetworkRequest };

View File

@@ -0,0 +1,23 @@
import axios from "axios";
const axiosWithProxy = async (req, { state }) => {
const { data } = await axios.post(
state.postwoman.settings.PROXY_URL || "https://postwoman.apollotv.xyz/",
req
);
return data;
};
const axiosWithoutProxy = async (req, _store) => {
const res = await axios(req);
return res;
};
const axiosStrategy = (req, store) => {
if (store.state.postwoman.settings.PROXY_ENABLED) {
return axiosWithProxy(req, store);
}
return axiosWithoutProxy(req, store);
};
export default axiosStrategy;

View File

@@ -0,0 +1,56 @@
const EXTENSION_ID = "amknoiejhlmhancpahfcfcfhllgkpbld";
// Check if the Chrome Extension is present
// The Chrome extension injects an empty span to help detection.
// Also check for the presence of window.chrome object to confirm smooth operations
export const hasChromeExtensionInstalled = () => {
return document.getElementById("chromePWExtensionDetect") !== null;
}
const chromeWithoutProxy = (req, _store) => new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
EXTENSION_ID, {
messageType: "send-req",
data: {
config: req
}
}, (message) => {
if (message.data.error) {
reject(message.data.error);
} else {
resolve(message.data.response);
}
}
);
});
const chromeWithProxy = (req, { state }) => new Promise((resolve, reject) => {
chrome.runtime.sendMessage(
EXTENSION_ID, {
messageType: "send-req",
data: {
config: {
method: "post",
url: state.postwoman.settings.PROXY_URL || "https://postwoman.apollotv.xyz/",
data: req
}
}
}, (message) => {
if (message.data.error) {
reject(error);
} else {
resolve(message.data.response.data);
}
}
)
});
const chromeStrategy = (req, store) => {
if (store.state.postwoman.settings.PROXY_ENABLED) {
return chromeWithProxy(req, store);
} else {
return chromeWithoutProxy(req, store);
}
}
export default chromeStrategy;

View File

@@ -0,0 +1,50 @@
const firefoxWithProxy = (req, { state }) =>
new Promise((resolve, reject) => {
const eventListener = event => {
window.removeEventListener("firefoxExtSendRequestComplete", event);
if (event.detail.error) {
reject(JSON.parse(event.detail.error));
} else {
resolve(JSON.parse(event.detail.response).data);
}
};
window.addEventListener("firefoxExtSendRequestComplete", eventListener);
window.firefoxExtSendRequest({
method: "post",
url:
state.postwoman.settings.PROXY_URL || "https://postwoman.apollotv.xyz/",
data: req
});
});
const firefoxWithoutProxy = (req, _store) =>
new Promise((resolve, reject) => {
const eventListener = ({ detail }) => {
window.removeEventListener(
"firefoxExtSendRequestComplete",
eventListener
);
if (detail.error) {
reject(JSON.parse(detail.error));
} else {
resolve(JSON.parse(detail.response));
}
};
window.addEventListener("firefoxExtSendRequestComplete", eventListener);
window.firefoxExtSendRequest(req);
});
const firefoxStrategy = (req, store) => {
if (store.state.postwoman.settings.PROXY_ENABLED) {
return firefoxWithProxy(req, store);
}
return firefoxWithoutProxy(req, store);
};
export default firefoxStrategy;

View File

@@ -0,0 +1,15 @@
// Debounce is a higher order function which makes its enclosed function be executed
// only if the function wasn't called again till 'delay' time has passed, this helps reduce impact of heavy working
// functions which might be called frequently
// NOTE : Don't use lambda functions as this doesn't get bound properly in them, use the 'function (args) {}' format
const debounce = (func, delay) => {
let inDebounce;
return function() {
const context = this;
const args = arguments;
clearTimeout(inDebounce);
inDebounce = setTimeout(() => func.apply(context, args), delay);
};
};
export default debounce;

View File

@@ -154,7 +154,8 @@ export default {
disconnected_from: "Disconnected from {name}",
something_went_wrong: "Something went wrong!",
error_occurred: "An error has occurred.",
browser_support_sse: "This browser doesn't seems to have Server Sent Events support.",
browser_support_sse:
"This browser doesn't seems to have Server Sent Events support.",
log: "Log",
no_url: "No URL",
run_query: "Run Query",
@@ -207,7 +208,8 @@ export default {
value_count: "value {count}",
send_request_first: "Send a request first",
generate_docs: "Generate Documentation",
generate_docs_message: "Import any Postwoman Collection to Generate Documentation on-the-go.",
generate_docs_message:
"Import any Postwoman Collection to Generate Documentation on-the-go.",
generate_docs_first: "Generate documentation first",
docs_generated: "Documentation generated",
import_collections: "Import collections",
@@ -231,8 +233,10 @@ export default {
enable_proxy: "Try enabling Proxy",
complete_config_urls: "Please complete configuration urls.",
token_request_saved: "Token request saved",
donate_info1: "If you have enjoyed the productivity of using Postwoman, consider donating as a sign of appreciation.",
donate_info2: "You can support Postwoman development via the following methods:",
donate_info1:
"If you have enjoyed the productivity of using Postwoman, consider donating as a sign of appreciation.",
donate_info2:
"You can support Postwoman development via the following methods:",
one_time_recurring: "One-time or recurring",
one_time: "One-time",
recurring: "Recurring",
@@ -241,5 +245,20 @@ export default {
go_home: "Go Home",
reload: "Reload",
enter_curl: "Enter cURL",
empty: "Empty"
empty: "Empty",
extensions: "Extensions",
extensions_info1: "Browser extension that simplifies access to Postwoman",
extensions_info2: "Get Postwoman browser extension!",
installed: "Installed",
login_with: "Login with",
logged_out: "Logged out",
logout: "Logout",
account: "Account",
sync: "Sync",
syncHistory: "History",
syncCollections: "Collections",
turn_on: "Turn on",
login_first: "Login first",
paste_a_collection: "Paste a Collection",
import_from_sync: "Import from Sync"
};

View File

@@ -237,6 +237,11 @@
<div v-else-if="$route.path === '/settings'">
<nav class="secondary-nav">
<ul>
<li>
<a href="#account" v-tooltip.right="$t('account')">
<i class="material-icons">person</i>
</a>
</li>
<li>
<a href="#theme" v-tooltip.right="$t('theme')">
<i class="material-icons">brush</i>
@@ -288,11 +293,61 @@
>
<i class="material-icons">offline_bolt</i>
</button>
<login v-if="!fb.currentUser" />
<span v-if="fb.currentUser">
<v-popover>
<button
class="icon"
v-tooltip="
(fb.currentUser.displayName ||
'<label><i>Name not found</i></label>') +
'<br>' +
(fb.currentUser.email ||
'<label><i>Email not found</i></label>')
"
>
<img
v-if="fb.currentUser.photoURL"
:src="fb.currentUser.photoURL"
class="material-icons"
/>
<i v-else class="material-icons">account_circle</i>
</button>
<template slot="popover">
<div>
<nuxt-link :to="localePath('settings')" v-close-popover>
<button class="icon">
<i class="material-icons">settings</i>
<span>
{{ $t("settings") }}
</span>
</button>
</nuxt-link>
</div>
<div>
<button class="icon" @click="logout" v-close-popover>
<i class="material-icons">exit_to_app</i>
<span>{{ $t("logout") }}</span>
</button>
</div>
</template>
</v-popover>
</span>
<v-popover>
<button class="icon" v-tooltip="$t('more')">
<i class="material-icons">more_vert</i>
</button>
<template slot="popover">
<div>
<button
class="icon"
@click="showExtensions = true"
v-close-popover
>
<i class="material-icons">extension</i>
<span>{{ $t("extensions") }}</span>
</button>
</div>
<div>
<button
class="icon"
@@ -342,15 +397,7 @@
<div class="flex-wrap">
<span v-if="version.name" class="mono">
<a
href="https://liyasthomas.web.app"
target="_blank"
rel="noopener"
>
<button class="icon" v-tooltip="'Liyas Thomas'">
🦄
</button>
</a>
<a
class="link"
:href="
'https://github.com/liyasthomas/postwoman/releases/tag/' +
version.name
@@ -361,6 +408,14 @@
>
{{ version.name }}
</a>
<a
class="link"
href="https://www.netlify.com"
target="_blank"
rel="noopener"
>
Powered by Netlify
</a>
<!-- <span v-if="version.hash">
-
<a
@@ -372,6 +427,15 @@
<!-- <span v-if="version.variant">({{version.variant}})</span> -->
</span>
<span>
<a
href="https://liyasthomas.web.app"
target="_blank"
rel="noopener"
>
<button class="icon" v-tooltip="'Liyas Thomas'">
🦄
</button>
</a>
<a
href="mailto:liyascthomas@gmail.com"
target="_blank"
@@ -402,6 +466,86 @@
<aside class="nav-second"></aside>
</div>
</div>
<modal v-if="showExtensions" @close="showExtensions = false">
<div slot="header">
<ul>
<li>
<div class="flex-wrap">
<h3 class="title">{{ $t("extensions") }}</h3>
<div>
<button class="icon" @click="showExtensions = false">
<i class="material-icons">close</i>
</button>
</div>
</div>
</li>
</ul>
</div>
<div slot="body">
<p class="info">
{{ $t("extensions_info1") }}
</p>
<div>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/postwoman"
target="_blank"
rel="noopener"
>
<button class="icon">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
d="M12 0c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm8.003 8.657c-1.276-3.321-4.46-4.605-5.534-4.537 3.529 1.376 4.373 6.059 4.06 7.441-.307-1.621-1.286-3.017-1.872-3.385 3.417 8.005-4.835 10.465-7.353 7.687.649.168 1.931.085 2.891-.557.898-.602.983-.638 1.56-.683.686-.053-.041-1.406-1.539-1.177-.616.094-1.632.819-2.88.341-1.508-.576-1.46-2.634.096-2.015.337-.437.088-1.263.088-1.263.452-.414 1.022-.706 1.37-.911.228-.135.829-.507.795-1.23-.123-.096-.32-.219-.766-.193-1.736.11-1.852-.518-1.967-.808.078-.668.524-1.534 1.361-1.931-1.257-.193-2.28.397-2.789 1.154-.809-.174-1.305-.183-2.118-.031-.316-.24-.666-.67-.878-1.181 1.832-2.066 4.499-3.378 7.472-3.378 5.912 0 8.263 4.283 8.003 6.657z"
/>
</svg>
<span>Firefox</span>
<span
class="icon"
v-if="firefoxExtInstalled"
v-tooltip="$t('installed')"
>
<i class="material-icons">done</i>
</span>
</button>
</a>
</div>
<div>
<a
href="https://chrome.google.com/webstore/detail/postwoman-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld"
target="_blank"
rel="noopener"
>
<button class="icon">
<svg
class="material-icons"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
d="M2.897 4.181c2.43-2.828 5.763-4.181 9.072-4.181 4.288 0 8.535 2.273 10.717 6.554-2.722.001-6.984 0-9.293 0-1.674.001-2.755-.037-3.926.579-1.376.724-2.415 2.067-2.777 3.644l-3.793-6.596zm5.11 7.819c0 2.2 1.789 3.99 3.988 3.99s3.988-1.79 3.988-3.99-1.789-3.991-3.988-3.991-3.988 1.791-3.988 3.991zm5.536 5.223c-2.238.666-4.858-.073-6.293-2.549-1.095-1.891-3.989-6.933-5.305-9.225-1.33 2.04-1.945 4.294-1.945 6.507 0 5.448 3.726 10.65 9.673 11.818l3.87-6.551zm2.158-9.214c1.864 1.734 2.271 4.542 1.007 6.719-.951 1.641-3.988 6.766-5.46 9.248 7.189.443 12.752-5.36 12.752-11.972 0-1.313-.22-2.66-.69-3.995h-7.609z"
/>
</svg>
<span>Chrome</span>
<span
class="icon"
v-if="chromeExtInstalled"
v-tooltip="$t('installed')"
>
<i class="material-icons">done</i>
</span>
</button>
</a>
</div>
</div>
<div slot="footer"></div>
</modal>
<modal v-if="showShortcuts" @close="showShortcuts = false">
<div slot="header">
<ul>
@@ -504,16 +648,30 @@
</div>
</template>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.link {
margin: 8px 16px;
}
@media (max-width: 768px) {
.link {
display: flex;
flex: 1;
}
}
</style>
<script>
import intializePwa from "../assets/js/pwa";
import * as version from "../.postwoman/version.json";
import { hasChromeExtensionInstalled } from "../functions/strategies/ChromeStrategy";
import firebase from "firebase/app";
import { fb } from "../functions/fb";
export default {
components: {
logo: () => import("../components/logo"),
modal: () => import("../components/modal")
modal: () => import("../components/modal"),
login: () => import("../components/firebase/login")
},
methods: {
@@ -522,6 +680,21 @@ export default {
"nuxt-link-exact-active": this.$route.path === path,
"nuxt-link-active": this.$route.path === path
};
},
logout() {
fb.currentUser = null;
firebase
.auth()
.signOut()
.catch(err => {
this.$toast.show(err.message || err, {
icon: "error"
});
});
this.$toast.info(this.$t("logged_out"), {
icon: "vpn_key"
});
}
},
@@ -532,8 +705,12 @@ export default {
// prompt.
showInstallPrompt: null,
version: {},
showExtensions: false,
showShortcuts: false,
showSupport: false
showSupport: false,
firefoxExtInstalled: window.firefoxExtSendRequest,
chromeExtInstalled: window.chrome && hasChromeExtensionInstalled(),
fb
};
},
@@ -591,6 +768,37 @@ export default {
]
});
}
let showExtensionsToast =
localStorage.getItem("showExtensionsToast") === "yes";
if (
!this.firefoxExtInstalled &&
!this.chromeExtInstalled &&
!showExtensionsToast
) {
setTimeout(() => {
this.$toast.show(this.$t("extensions_info2"), {
icon: "extension",
duration: 5000,
theme: "toasted-primary",
action: [
{
text: this.$t("yes"),
onClick: (e, toastObject) => {
this.showExtensions = true;
localStorage.setItem("showExtensionsToast", "yes");
toastObject.goAway(0);
}
},
{
text: this.$t("no"),
onClick: (e, toastObject) => {
toastObject.goAway(0);
}
}
]
});
}, 15000);
}
})();
window.addEventListener("scroll", event => {
@@ -623,7 +831,7 @@ export default {
watch: {
$route() {
this.$toast.clear();
// this.$toast.clear();
}
},

View File

@@ -1,6 +1,10 @@
<template>
<div class="page page-error">
<img src="~static/icons/error.svg" :alt="$t('error')" class="error_banner" />
<img
src="~static/icons/error.svg"
:alt="$t('error')"
class="error_banner"
/>
<h2>{{ error.statusCode }}</h2>
<h3>{{ error.message }}</h3>
<p>

1104
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,27 +18,29 @@
"test": "start-server-and-test dev http://localhost:3000 e2e"
},
"dependencies": {
"@nuxtjs/axios": "^5.9.2",
"@nuxtjs/axios": "^5.9.3",
"@nuxtjs/google-analytics": "^2.2.3",
"@nuxtjs/google-tag-manager": "^2.3.1",
"@nuxtjs/pwa": "^3.0.0-beta.19",
"@nuxtjs/robots": "^2.4.2",
"@nuxtjs/sitemap": "^2.0.1",
"@nuxtjs/toast": "^3.3.0",
"ace-builds": "^1.4.7",
"ace-builds": "^1.4.8",
"firebase": "^7.7.0",
"graphql": "^14.5.8",
"nuxt": "^2.11.0",
"nuxt-i18n": "^6.4.1",
"v-tooltip": "^2.0.2",
"nuxt-i18n": "^6.5.0",
"v-tooltip": "^2.0.3",
"vue-virtual-scroll-list": "^1.4.4",
"vuefire": "^2.2.1",
"vuejs-auto-complete": "^0.9.0",
"vuex-persist": "^2.2.0",
"yargs-parser": "^16.1.0"
},
"devDependencies": {
"cypress": "^3.8.2",
"node-sass": "^4.13.0",
"sass-loader": "^8.0.1",
"cypress": "^3.8.3",
"node-sass": "^4.13.1",
"sass-loader": "^8.0.2",
"start-server-and-test": "^1.10.6"
}
}

View File

@@ -93,12 +93,14 @@
</p>
<p class="doc-desc" v-if="request.path">
<span>
{{ $t("path") }}: <code>{{ request.path || $t("none") }}</code>
{{ $t("path") }}:
<code>{{ request.path || $t("none") }}</code>
</span>
</p>
<p class="doc-desc" v-if="request.method">
<span>
{{ $t("method") }}: <code>{{ request.method || $t("none") }}</code>
{{ $t("method") }}:
<code>{{ request.method || $t("none") }}</code>
</span>
</p>
<p class="doc-desc" v-if="request.auth">
@@ -109,7 +111,8 @@
</p>
<p class="doc-desc" v-if="request.httpUser">
<span>
{{ $t("username") }}: <code>{{ request.httpUser || $t("none") }}</code>
{{ $t("username") }}:
<code>{{ request.httpUser || $t("none") }}</code>
</span>
</p>
<p class="doc-desc" v-if="request.httpPassword">
@@ -120,16 +123,17 @@
</p>
<p class="doc-desc" v-if="request.bearerToken">
<span>
{{ $t("token") }}: <code>{{ request.bearerToken || $t("none") }}</code>
{{ $t("token") }}:
<code>{{ request.bearerToken || $t("none") }}</code>
</span>
</p>
<h4 v-if="request.headers.length > 0">{{ $t("headers") }}</h4>
<span
v-if="request.headers"
v-for="header in request.headers"
:key="header.key"
>
<p class="doc-desc">
<span v-if="request.headers">
<p
v-for="header in request.headers"
:key="header.key"
class="doc-desc"
>
<span>
{{ header.key || $t("none") }}:
<code>{{ header.value || $t("none") }}</code>
@@ -137,12 +141,12 @@
</p>
</span>
<h4 v-if="request.params.length > 0">{{ $t("parameters") }}</h4>
<span
v-if="request.params"
v-for="parameter in request.params"
:key="parameter.key"
>
<p class="doc-desc">
<span v-if="request.params">
<p
v-for="parameter in request.params"
:key="parameter.key"
class="doc-desc"
>
<span>
{{ parameter.key || $t("none") }}:
<code>{{ parameter.value || $t("none") }}</code>
@@ -150,12 +154,12 @@
</p>
</span>
<h4 v-if="request.bodyParam">{{ $t("payload") }}</h4>
<span
v-if="request.bodyParam"
v-for="payload in request.bodyParam"
:key="payload.key"
>
<p class="doc-desc">
<span v-if="request.bodyParam">
<p
v-for="payload in request.bodyParam"
:key="payload.key"
class="doc-desc"
>
<span>
{{ payload.key || $t("none") }}:
<code>{{ payload.value || $t("none") }}</code>
@@ -164,7 +168,8 @@
</span>
<p class="doc-desc" v-if="request.rawParams">
<span>
{{ $t("parameters") }}: <code>{{ request.rawParams || $t("none") }}</code>
{{ $t("parameters") }}:
<code>{{ request.rawParams || $t("none") }}</code>
</span>
</p>
<p class="doc-desc" v-if="request.contentType">
@@ -202,7 +207,8 @@
</p>
<p class="doc-desc" v-if="request.method">
<span>
{{ $t("method") }}: <code>{{ request.method || $t("none") }}</code>
{{ $t("method") }}:
<code>{{ request.method || $t("none") }}</code>
</span>
</p>
<p class="doc-desc" v-if="request.auth">
@@ -213,26 +219,29 @@
</p>
<p class="doc-desc" v-if="request.httpUser">
<span>
{{ $t("username") }}: <code>{{ request.httpUser || $t("none") }}</code>
{{ $t("username") }}:
<code>{{ request.httpUser || $t("none") }}</code>
</span>
</p>
<p class="doc-desc" v-if="request.httpPassword">
<span>
{{ $t("password") }}: <code>{{ request.httpPassword || $t("none") }}</code>
{{ $t("password") }}:
<code>{{ request.httpPassword || $t("none") }}</code>
</span>
</p>
<p class="doc-desc" v-if="request.bearerToken">
<span>
{{ $t("token") }}: <code>{{ request.bearerToken || $t("none") }}</code>
{{ $t("token") }}:
<code>{{ request.bearerToken || $t("none") }}</code>
</span>
</p>
<h4 v-if="request.headers.length > 0">{{ $t("headers") }}</h4>
<span
v-if="request.headers"
v-for="header in request.headers"
:key="header.key"
>
<p class="doc-desc">
<span v-if="request.headers">
<p
v-for="header in request.headers"
:key="header.key"
class="doc-desc"
>
<span>
{{ header.key || $t("none") }}:
<code>{{ header.value || $t("none") }}</code>
@@ -240,12 +249,12 @@
</p>
</span>
<h4 v-if="request.params.length > 0">{{ $t("parameters") }}</h4>
<span
v-if="request.params"
v-for="parameter in request.params"
:key="parameter.key"
>
<p class="doc-desc">
<span v-if="request.params">
<p
v-for="parameter in request.params"
:key="parameter.key"
class="doc-desc"
>
<span>
{{ parameter.key || $t("none") }}:
<code>{{ parameter.value || $t("none") }}</code>
@@ -253,12 +262,12 @@
</p>
</span>
<h4 v-if="request.bodyParam">{{ $t("payload") }}</h4>
<span
v-if="request.bodyParam"
v-for="payload in request.bodyParam"
:key="payload.key"
>
<p class="doc-desc">
<span v-if="request.bodyParam">
<p
v-for="payload in request.bodyParam"
:key="payload.key"
class="doc-desc"
>
<span>
{{ payload.key || $t("none") }}:
<code>{{ payload.value || $t("none") }}</code>
@@ -267,7 +276,8 @@
</span>
<p class="doc-desc" v-if="request.rawParams">
<span>
{{ $t("parameters") }}: <code>{{ request.rawParams || $t("none") }}</code>
{{ $t("parameters") }}:
<code>{{ request.rawParams || $t("none") }}</code>
</span>
</p>
<p class="doc-desc" v-if="request.contentType">
@@ -305,9 +315,6 @@
}
}
.collection {
}
.folder {
border-left: 1px solid var(--brd-color);
margin: 16px 0 0;

View File

@@ -175,7 +175,8 @@
</button>
</div>
</div>
<Editor
<QueryEditor
ref="queryEditor"
v-model="gqlQueryString"
:options="{
maxLines: responseBodyMaxLines,
@@ -391,6 +392,8 @@ import axios from "axios";
import * as gql from "graphql";
import textareaAutoHeight from "../directives/textareaAutoHeight";
import AceEditor from "../components/ace-editor";
import QueryEditor from "../components/graphql/queryeditor";
import { sendNetworkRequest } from "../functions/network";
export default {
directives: {
@@ -401,7 +404,8 @@ export default {
"gql-field": () => import("../components/graphql/field"),
"gql-type": () => import("../components/graphql/type"),
autocomplete: () => import("../components/autocomplete"),
Editor: AceEditor
Editor: AceEditor,
QueryEditor: QueryEditor
},
data() {
return {
@@ -542,6 +546,7 @@ export default {
responseBodyMaxLines: 16
};
},
computed: {
url: {
get() {
@@ -666,14 +671,9 @@ export default {
const gqlQueryString = this.gqlQueryString;
this.variables.forEach(variable => {
// todo: better variable type validation
const intRex = new RegExp(`\$${variable.key}\: Int`);
intRex.compile();
const floatRex = new RegExp(`\$${variable.key}\: Float`);
floatRex.compile();
if (intRex.test(gqlQueryString)) {
if (gqlQueryString.indexOf(`\$${variable.key}: Int`) > -1) {
variables[variable.key] = parseInt(variable.value);
} else if (floatRex.test(gqlQueryString)) {
} else if (gqlQueryString.indexOf(`\$${variable.key}: Float`) > -1) {
variables[variable.key] = parseFloat(variable.value);
} else {
variables[variable.key] = variable.value;
@@ -690,21 +690,7 @@ export default {
data: JSON.stringify({ query: gqlQueryString, variables })
};
const reqConfig = this.$store.state.postwoman.settings.PROXY_ENABLED
? {
method: "post",
url:
this.$store.state.postwoman.settings.PROXY_URL ||
`https://postwoman.apollotv.xyz/`,
data: reqOptions
}
: reqOptions;
const res = await axios(reqConfig);
const data = this.$store.state.postwoman.settings.PROXY_ENABLED
? res.data
: res;
const data = await sendNetworkRequest(reqOptions, this.$store);
this.responseString = JSON.stringify(data.data, null, 2);
@@ -826,7 +812,7 @@ export default {
}
}
this.gqlTypes = types;
this.$refs.queryEditor.setValidationSchema(schema);
this.$nuxt.$loading.finish();
const duration = Date.now() - startTime;
this.$toast.info(this.$t("finished_in", { duration }), {
@@ -834,7 +820,7 @@ export default {
});
} catch (error) {
this.$nuxt.$loading.finish();
this.schemaString = `${error}. ${check_console_details}`;
this.schemaString = `${error}. ${this.$t("check_console_details")}`;
this.$toast.error(`${error} ${this.$t("f12_details")}`, {
icon: "error"
});

View File

@@ -40,11 +40,7 @@
</ul>
</pw-section>
<pw-section
class="blue"
:label="$t('request')"
ref="request"
>
<pw-section class="blue" :label="$t('request')" ref="request">
<ul>
<li>
<label for="method">{{ $t("method") }}</label>
@@ -842,10 +838,34 @@
<input id="collection-tab" type="radio" name="side" />
<label for="collection-tab">{{ $t("collections") }}</label>
<div class="tab">
<pw-section class="yellow" :label="$t('collections')" ref="collections">
<pw-section
class="yellow"
:label="$t('collections')"
ref="collections"
>
<collections />
</pw-section>
</div>
<input id="sync-tab" type="radio" name="side" />
<label for="sync-tab">{{ $t("sync") }}</label>
<div class="tab">
<pw-section
v-if="fb.currentUser"
class="pink"
label="Sync"
ref="sync"
>
<inputform />
<ballsfeed />
</pw-section>
<pw-section v-else>
<ul>
<li>
<label>{{ $t("login_first") }}</label>
</li>
</ul>
</pw-section>
</div>
</section>
</aside>
@@ -1135,6 +1155,8 @@ import getEnvironmentVariablesFromScript from "../functions/preRequest";
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";
const statusCategories = [
{
@@ -1199,7 +1221,9 @@ export default {
autocomplete: () => import("../components/autocomplete"),
collections: () => import("../components/collections"),
saveRequestAs: () => import("../components/collections/saveRequestAs"),
Editor: AceEditor
Editor: AceEditor,
inputform: () => import("../components/firebase/inputform"),
ballsfeed: () => import("../components/firebase/feeds")
},
data() {
return {
@@ -1369,12 +1393,12 @@ export default {
],
showRequestModal: false,
editRequest: {},
urlExcludes: {},
responseBodyText: "",
responseBodyType: "text",
responseBodyMaxLines: 16,
activeSidebar: true
activeSidebar: true,
fb
};
},
watch: {
@@ -1992,20 +2016,8 @@ export default {
if (typeof requestOptions.data === "string") {
requestOptions.data = parseTemplateString(requestOptions.data);
}
const config = this.$store.state.postwoman.settings.PROXY_ENABLED
? {
method: "POST",
url:
this.$store.state.postwoman.settings.PROXY_URL ||
"https://postwoman.apollotv.xyz/",
data: requestOptions
}
: requestOptions;
const response = await this.$axios(config);
return this.$store.state.postwoman.settings.PROXY_ENABLED
? response.data
: response;
return await sendNetworkRequest(requestOptions, this.$store);
},
async sendRequest() {
this.$toast.clear();
@@ -2117,6 +2129,11 @@ export default {
star: false
};
this.$refs.historyComponent.addEntry(entry);
if (fb.currentUser !== null) {
if (fb.currentSettings[1].value) {
fb.writeHistory(entry);
}
}
})();
} catch (error) {
console.error(error);
@@ -2138,6 +2155,11 @@ export default {
preRequestScript: this.preRequestScript
};
this.$refs.historyComponent.addEntry(entry);
if (fb.currentUser !== null) {
if (fb.currentSettings[1].value) {
fb.writeHistory(entry);
}
}
return;
} else {
this.response.status = error.message;
@@ -2150,7 +2172,7 @@ export default {
icon: "help",
duration: 8000,
action: {
text: "Settings",
text: this.$t("yes"),
onClick: (e, toastObject) => {
this.$router.push({ path: "/settings" });
}

View File

@@ -1,5 +1,88 @@
<template>
<div class="page">
<pw-section class="green" :label="$t('account')" ref="account">
<ul>
<li>
<div v-if="fb.currentUser">
<button class="icon">
<img
v-if="fb.currentUser.photoURL"
:src="fb.currentUser.photoURL"
class="material-icons"
/>
<i v-else class="material-icons">account_circle</i>
<span>
{{ fb.currentUser.displayName || "Name not found" }}
</span>
</button>
<br />
<button class="icon">
<i class="material-icons">email</i>
<span>
{{ fb.currentUser.email || "Email not found" }}
</span>
</button>
<br />
<button class="icon" @click="logout">
<i class="material-icons">exit_to_app</i>
<span>{{ $t("logout") }}</span>
</button>
<br />
<p v-for="setting in fb.currentSettings" :key="setting.id">
<pw-toggle
:key="setting.name"
:on="setting.value"
@change="toggleSettings(setting.name, setting.value)"
>
{{ $t(setting.name) + " " + $t("sync") }}
{{ setting.value ? $t("enabled") : $t("disabled") }}
</pw-toggle>
</p>
<p v-if="fb.currentSettings.length == 0">
<button class="" @click="initSettings">
<i class="material-icons">sync</i>
<span>{{ $t("turn_on") + " " + $t("sync") }}</span>
</button>
</p>
</div>
<div v-else>
<label>{{ $t("login_with") }}</label>
<p>
<button class="icon" @click="signInWithGoogle">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="material-icons"
>
<path
d="M12.24 10.285V14.4h6.806c-.275 1.765-2.056 5.174-6.806 5.174-4.095 0-7.439-3.389-7.439-7.574s3.345-7.574 7.439-7.574c2.33 0 3.891.989 4.785 1.849l3.254-3.138C18.189 1.186 15.479 0 12.24 0c-6.635 0-12 5.365-12 12s5.365 12 12 12c6.926 0 11.52-4.869 11.52-11.726 0-.788-.085-1.39-.189-1.989H12.24z"
/>
</svg>
<span>Google</span>
</button>
<br />
<button class="icon" @click="signInWithGithub">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="material-icons"
>
<path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
</svg>
<span>GitHub</span>
</button>
</p>
</div>
</li>
</ul>
</pw-section>
<pw-section class="cyan" :label="$t('theme')" ref="theme">
<ul>
<li>
@@ -137,6 +220,9 @@
<style scoped lang="scss"></style>
<script>
import firebase from "firebase/app";
import { fb } from "../functions/fb";
export default {
components: {
"pw-section": () => import("../components/section"),
@@ -238,7 +324,9 @@ export default {
this.$store.state.postwoman.settings.PROXY_URL ||
"https://postwoman.apollotv.xyz/",
PROXY_KEY: this.$store.state.postwoman.settings.PROXY_KEY || ""
}
},
fb
};
},
@@ -289,8 +377,88 @@ export default {
toggleSetting(key) {
this.settings[key] = !this.settings[key];
this.$store.commit("postwoman/applySetting", [key, this.settings[key]]);
},
logout() {
fb.currentUser = null;
firebase
.auth()
.signOut()
.catch(err => {
this.$toast.show(err.message || err, {
icon: "error"
});
});
this.$toast.info(this.$t("logged_out"), {
icon: "vpn_key"
});
},
signInWithGoogle() {
const provider = new firebase.auth.GoogleAuthProvider();
firebase
.auth()
.signInWithPopup(provider)
.then(res => {
if (res.additionalUserInfo.isNewUser) {
this.$toast.info(this.$t("turn_on") + " " + this.$t("sync"), {
icon: "sync",
duration: null,
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: (e, toastObject) => {
fb.writeSettings("syncHistory", true);
fb.writeSettings("syncCollections", false);
this.$router.push({ path: "/settings" });
toastObject.remove();
}
}
});
}
})
.catch(err => {
this.$toast.show(err.message || err, {
icon: "error"
});
});
},
signInWithGithub() {
const provider = new firebase.auth.GithubAuthProvider();
firebase
.auth()
.signInWithPopup(provider)
.then(res => {
if (res.additionalUserInfo.isNewUser) {
this.$toast.info(this.$t("turn_on") + " " + this.$t("sync"), {
icon: "sync",
duration: null,
closeOnSwipe: false,
action: {
text: this.$t("yes"),
onClick: (e, toastObject) => {
fb.writeSettings("syncHistory", true);
fb.writeSettings("syncCollections", false);
this.$router.push({ path: "/settings" });
toastObject.remove();
}
}
});
}
})
.catch(err => {
this.$toast.show(err.message || err, {
icon: "error"
});
});
},
toggleSettings(s, v) {
fb.writeSettings(s, !v);
},
initSettings() {
fb.writeSettings("syncHistory", true);
fb.writeSettings("syncCollections", false);
}
},
beforeMount() {
this.settings.THEME_COLOR = this.getActiveColor();
},

View File

@@ -111,6 +111,12 @@ export const mutations = {
},
addNewCollection({ collections }, collection) {
const { name } = collection;
const duplicateCollection = collections.some(item => item.name === name);
if (duplicateCollection) {
this.$toast.info("Duplicate collection");
return;
}
collections.push({
name: "",
folders: [],
@@ -126,6 +132,12 @@ export const mutations = {
editCollection({ collections }, payload) {
const { collection, collectionIndex } = payload;
const { name } = collection;
const duplicateCollection = collections.some(item => item.name === name);
if (duplicateCollection) {
this.$toast.info("Duplicate collection");
return;
}
collections[collectionIndex] = collection;
},