diff --git a/README.md b/README.md index 0093dee15..fa60e90b5 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ _**All `i18n` contributions are welcome to `i18n` [branch](https://github.com/li - **[CLI β](https://github.com/postwoman-io/postwoman-cli)** - A CLI solution for Postwoman - **Browser Extensions** - Browser extensions that simplifies access to Postwoman - [![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))  |  [![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)) + [![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_16x16.png) **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/postwoman)  |  [![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-extension)) >**Extensions fixes `CORS` issues.** diff --git a/assets/css/styles.scss b/assets/css/styles.scss index 2bb3bba07..ef24d4e81 100644 --- a/assets/css/styles.scss +++ b/assets/css/styles.scss @@ -22,13 +22,11 @@ $responsiveWidth: 768px; ::-webkit-scrollbar { width: 4px; height: 4px; - border-radius: 4px; - background-color: var(--bg-light-color); + background-color: var(--bg-dark-color); } ::-webkit-scrollbar-thumb { background-color: var(--fg-light-color); - border-radius: 8px; &:hover { background-color: var(--fg-color); @@ -60,9 +58,9 @@ body { padding: 0; margin: 0; scroll-behavior: smooth; + transition: all 0.2s ease-in-out; } -// Make theme transition smoother. body.afterLoad { transition: background-color 0.2s ease-in-out; } @@ -76,6 +74,10 @@ a { color: inherit; text-decoration: none; transition: all 0.2s ease-in-out; + + &.link { + color: var(--ac-color); + } } header, @@ -146,7 +148,8 @@ footer { z-index: 1; height: 100vh; padding: 0 8px; - background-color: var(--bg-light-color); + background-color: var(--bg-dark-color); + transition: all 0.2s ease-in-out; } .main { @@ -173,6 +176,7 @@ nav.primary-nav { svg { fill: var(--fg-light-color); + transition: all 0.2s ease-in-out; } a { @@ -186,7 +190,6 @@ nav.primary-nav { color: var(--fg-light-color); fill: var(--fg-light-color); margin: 8px 0; - transition: all 0.2s ease-in-out; &:hover { color: var(--fg-color); @@ -263,11 +266,16 @@ hr { border-bottom: 1px dashed var(--brd-color); } +p { + transition: all 0.2s ease-in-out; +} + .tooltip { $bgcolor: var(--tt-color); $fgcolor: var(--fg-color); display: block !important; z-index: 10000; + transition: all 0.2s ease-in-out; .tooltip-inner { background: $bgcolor; @@ -422,7 +430,6 @@ button { color: var(--act-color); fill: var(--act-color); box-shadow: inset 0 0 0 2px var(--fg-color); - transition: all 0.2s ease-in-out; } &.icon { @@ -437,7 +444,6 @@ button { color: var(--fg-color); fill: var(--fg-color); box-shadow: none; - transition: all 0.2s ease-in-out; } } @@ -472,8 +478,19 @@ button { fieldset { margin: 16px 0; border-radius: 16px; - transition: all 0.2s ease-in-out; background-color: var(--bg-dark-color); + transition: all 0.2s ease-in-out; +} + +fieldset:target, +section:target { + animation: highlight 2s ease; +} + +@keyframes highlight { + 50% { + box-shadow: 0 0 0 2px var(--ac-color); + } } legend { @@ -483,6 +500,7 @@ legend { color: var(--fg-color); font-weight: 700; cursor: pointer; + transition: all 0.2s ease-in-out; * { vertical-align: middle; @@ -565,7 +583,6 @@ code { &:not([readonly]):not(.ace_editor):active, &:not([readonly]):not(.ace_editor):focus { box-shadow: inset 0 0 0 2px var(--fg-light-color); - transition: all 0.2s ease-in-out; } } @@ -576,7 +593,6 @@ code { &:active, &:focus { box-shadow: inset 0 0 0 2px var(--fg-light-color); - transition: all 0.2s ease-in-out; } } @@ -681,6 +697,7 @@ input[type="checkbox"] { label { padding: 4px; color: var(--fg-light-color); + transition: all 0.2s ease-in-out; } ul, @@ -805,6 +822,7 @@ ol li { section { display: flex; flex-wrap: wrap; + border-radius: 16px; } .tab { @@ -884,18 +902,19 @@ input[type="radio"]:checked + label + .tab { padding: 0; width: 100%; background-color: var(--bg-color); - transition: all 0.2s ease-in-out; box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.45); } nav.primary-nav { flex-flow: row nowrap; overflow: auto; - justify-content: space-around; + justify-content: space-between; + background-color: var(--bg-dark-color); a { background-color: transparent; margin: 8px; + flex: 1; &.nuxt-link-exact-active { background-color: transparent; diff --git a/components/collections/importExportCollections.vue b/components/collections/importExportCollections.vue index 8df70d36d..6d79fb21e 100644 --- a/components/collections/importExportCollections.vue +++ b/components/collections/importExportCollections.vue @@ -120,20 +120,40 @@ export default { reader.onload = event => { let content = event.target.result; let collections = JSON.parse(content); - this.$store.commit("postwoman/replaceCollections", collections); + if (collections[0]) { + let [ name, folders, requests ] = Object.keys(collections[0]) + if (name === 'name' && folders === 'folders' && requests === 'requests') { + // Do nothing + } + } else if (collections.info && collections.info.schema.includes('v2.1.0')) { + collections = this.parsePostmanCollection(collections) + } else { + return this.failedImport(); + } + this.$store.commit("postwoman/importCollections", collections); + this.fileImported(); }; reader.readAsText(this.$refs.inputChooseFileToReplaceWith.files[0]); - this.fileImported(); }, importFromJSON() { let reader = new FileReader(); reader.onload = event => { let content = event.target.result; let collections = JSON.parse(content); + if (collections[0]) { + let [ name, folders, requests ] = Object.keys(collections[0]) + if (name === 'name' && folders === 'folders' && requests === 'requests') { + // Do nothing + } + } else if (collections.info && collections.info.schema.includes('v2.1.0')) { + collections = this.parsePostmanCollection(collections); + } else { + return this.failedImport(); + } this.$store.commit("postwoman/importCollections", collections); + this.fileImported(); }; reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0]); - this.fileImported(); }, exportJSON() { let text = this.collectionJson; @@ -161,6 +181,114 @@ export default { this.$toast.info(this.$t("file_imported"), { icon: "folder_shared" }); + }, + failedImport() { + this.$toast.error(this.$t("import_failed"), { + icon: "error" + }); + }, + parsePostmanCollection(collection, folders = true) { + let postwomanCollection = folders ? [{ + "name": "", + "folders": [], + "requests": [] + }] + : { + "name": "", + "requests": [] + }; + for(let collectionItem of collection.item) { + if (collectionItem.request) { + if (postwomanCollection[0]) { + postwomanCollection[0].name = collection.info ? collection.info.name : ""; + postwomanCollection[0].requests.push(this.parsePostmanRequest(collectionItem)); + } else { + postwomanCollection.name = collection.name ? collection.name + : ""; + postwomanCollection.requests.push(this.parsePostmanRequest(collectionItem)); + } + } else if (collectionItem.item) { + if (collectionItem.item[0]) { + postwomanCollection[0].folders.push(this.parsePostmanCollection(collectionItem, false)); + } + } + } + return postwomanCollection; + }, + parsePostmanRequest(requestObject) { + let pwRequest = { + "url": "", + "path": "", + "method": "", + "auth": "", + "httpUser": "", + "httpPassword": "", + "passwordFieldType": "password", + "bearerToken": "", + "headers": [], + "params": [], + "bodyParams": [], + "rawParams": "", + "rawInput": false, + "contentType": "", + "requestType": "", + "name": "", + }; + + pwRequest.name = requestObject.name; + let requestObjectUrl = requestObject.request.url.raw.match(/^(.+:\/\/[^\/]+|{[^\/]+})(\/[^\?]+|).*$/); + pwRequest.url = requestObjectUrl[1]; + pwRequest.path = requestObjectUrl[2] ? requestObjectUrl[2] : ""; + pwRequest.method = requestObject.request.method; + let itemAuth = requestObject.request.auth ? requestObject.request.auth + : ""; + let authType = itemAuth ? itemAuth.type + : ""; + if (authType === "basic") { + pwRequest.auth = "Basic Auth"; + pwRequest.httpUser = itemAuth.basic[0].key === "username" + ? itemAuth.basic[0].value + : itemAuth.basic[1].value; + pwRequest.httpPassword = itemAuth.basic[0].key === "password" + ? itemAuth.basic[0].value + : itemAuth.basic[1].value; + } else if (authType === "oauth2") { + pwRequest.auth = "OAuth 2.0"; + pwRequest.bearerToken = itemAuth.oauth2[0].key === "accessToken" + ? itemAuth.oauth2[0].value + : itemAuth.oauth2[1].value; + } else if (authType === "bearer") { + pwRequest.auth = "Bearer Token"; + pwRequest.bearerToken = itemAuth.bearer[0].value; + } + let requestObjectHeaders = requestObject.request.header; + if (requestObjectHeaders) { + pwRequest.headers = requestObjectHeaders; + for (let header of pwRequest.headers) { + delete header.name; + delete header.type; + } + } + let requestObjectParams = requestObject.request.url.query; + if (requestObjectParams) { + pwRequest.params = requestObjectParams; + for (let param of pwRequest.params) { + delete param.disabled; + } + } + if (requestObject.request.body) { + if (requestObject.request.body.mode === "urlencoded") { + let params = requestObject.request.body.urlencoded; + pwRequest.bodyParams = params ? params : []; + for(let param of pwRequest.bodyParams) { + delete param.type; + } + } else if (requestObject.request.body.mode === "raw") { + pwRequest.rawInput = true; + pwRequest.rawParams = requestObject.request.body.raw; + } + } + return pwRequest; } } }; diff --git a/components/firebase/login.vue b/components/firebase/login.vue index 0f06d87b2..64581db34 100644 --- a/components/firebase/login.vue +++ b/components/firebase/login.vue @@ -57,9 +57,9 @@ export default { firebase .auth() .signInWithPopup(provider) - .then(res => { - if (res.additionalUserInfo.isNewUser) { - this.$toast.info(this.$t("turn_on") + " " + this.$t("sync"), { + .then(({ additionalUserInfo }) => { + if (additionalUserInfo.isNewUser) { + this.$toast.info(`${this.$t("turn_on")} ${this.$t("sync")}`, { icon: "sync", duration: null, closeOnSwipe: false, @@ -86,9 +86,9 @@ export default { firebase .auth() .signInWithPopup(provider) - .then(res => { - if (res.additionalUserInfo.isNewUser) { - this.$toast.info(this.$t("turn_on") + " " + this.$t("sync"), { + .then(({ additionalUserInfo }) => { + if (additionalUserInfo.isNewUser) { + this.$toast.info(`${this.$t("turn_on")} ${this.$t("sync")}`, { icon: "sync", duration: null, closeOnSwipe: false, diff --git a/components/graphql/queryeditor.vue b/components/graphql/queryeditor.vue index 7a4e90c65..56fe6bc35 100644 --- a/components/graphql/queryeditor.vue +++ b/components/graphql/queryeditor.vue @@ -7,7 +7,9 @@ const DEFAULT_THEME = "twilight"; import ace from "ace-builds"; import * as gql from "graphql"; +import { getAutocompleteSuggestions } from "graphql-language-service-interface"; import "ace-builds/webpack-resolver"; +import "ace-builds/src-noconflict/ext-language_tools"; import debounce from "../../functions/utils/debounce"; export default { @@ -46,10 +48,10 @@ export default { } }, theme() { - this.editor.setTheme("ace/theme/" + this.defineTheme()); + this.editor.setTheme(`ace/theme/${this.defineTheme()}`); }, lang(value) { - this.editor.getSession().setMode("ace/mode/" + value); + this.editor.getSession().setMode(`ace/mode/${value}`); }, options(value) { this.editor.setOptions(value); @@ -57,12 +59,48 @@ export default { }, mounted() { + let langTools = ace.require("ace/ext/language_tools"); + const editor = ace.edit(this.$refs.editor, { - theme: "ace/theme/" + this.defineTheme(), - mode: "ace/mode/" + this.lang, + theme: `ace/theme/${this.defineTheme()}`, + mode: `ace/mode/${this.lang}`, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true, ...this.options }); + const completer = { + getCompletions: ( + editor, + _session, + { row, column }, + _prefix, + callback + ) => { + if (this.validationSchema) { + const completions = getAutocompleteSuggestions( + this.validationSchema, + editor.getValue(), + { line: row, character: column } + ); + + callback( + null, + completions.map(({ label, detail }) => ({ + name: label, + value: label, + score: 1.0, + meta: detail + })) + ); + } else { + callback(null, []); + } + } + }; + + langTools.setCompleters([completer]); + if (this.value) editor.setValue(this.value, 1); this.editor = editor; @@ -101,14 +139,14 @@ export default { 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, + gql + .validate(this.validationSchema, doc) + .map(({ locations, message }) => ({ + row: locations[0].line - 1, + column: locations[0].column - 1, + text: message, type: "error" - }; - }) + })) ); } } catch (e) { diff --git a/components/modal.vue b/components/modal.vue index c80b24be9..4b12fa291 100644 --- a/components/modal.vue +++ b/components/modal.vue @@ -25,7 +25,7 @@ left: 0; width: 100%; height: 100%; - background-color: rgba(0, 0, 0, 0.87); + background-color: rgba(0, 0, 0, 0.32); display: flex; align-items: center; justify-content: center; diff --git a/components/settings/swatch.vue b/components/settings/swatch.vue index 0d0dc870b..55373c58d 100644 --- a/components/settings/swatch.vue +++ b/components/settings/swatch.vue @@ -20,6 +20,7 @@ border-radius: 100%; border: 3px solid var(--bg-dark-color); cursor: pointer; + transition: all 0.2s ease-in-out; &.fg { color: var(--act-color); diff --git a/functions/headers.js b/functions/headers.js index 9fbc2938e..ffc6b5d5f 100644 --- a/functions/headers.js +++ b/functions/headers.js @@ -121,4 +121,4 @@ export const commonHeaders = [ "X-Requested-With", "X-Robots-Tag", "X-UA-Compatible" -] +]; diff --git a/functions/network.js b/functions/network.js index 9294ba036..57cc634b8 100644 --- a/functions/network.js +++ b/functions/network.js @@ -1,18 +1,28 @@ import AxiosStrategy from "./strategies/AxiosStrategy"; +import ExtensionStrategy, { + hasExtensionInstalled +} from "./strategies/ExtensionStrategy"; import FirefoxStrategy from "./strategies/FirefoxStrategy"; -import ChromeStrategy, { hasChromeExtensionInstalled } from "./strategies/ChromeStrategy"; +import ChromeStrategy, { + hasChromeExtensionInstalled +} from "./strategies/ChromeStrategy"; -const isExtensionsAllowed = ({ state }) => { - return typeof state.postwoman.settings.EXTENSIONS_ENABLED === 'undefined' - || state.postwoman.settings.EXTENSIONS_ENABLED; -} +const isExtensionsAllowed = ({ state }) => + typeof state.postwoman.settings.EXTENSIONS_ENABLED === "undefined" || + state.postwoman.settings.EXTENSIONS_ENABLED; const runAppropriateStrategy = (req, store) => { if (isExtensionsAllowed(store)) { + if (hasExtensionInstalled()) { + return ExtensionStrategy(req, store); + } + + // The following strategies are deprecated and kept to support older version of the extensions + // 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); + 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 @@ -22,10 +32,11 @@ const runAppropriateStrategy = (req, store) => { } return AxiosStrategy(req, store); -} +}; const sendNetworkRequest = (req, store) => - runAppropriateStrategy(req, store) - .finally(() => window.$nuxt.$loading.finish()); + runAppropriateStrategy(req, store).finally(() => + window.$nuxt.$loading.finish() + ); export { sendNetworkRequest }; diff --git a/functions/strategies/ChromeStrategy.js b/functions/strategies/ChromeStrategy.js index 03c05ea0c..fe88338cb 100644 --- a/functions/strategies/ChromeStrategy.js +++ b/functions/strategies/ChromeStrategy.js @@ -3,47 +3,54 @@ 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; -} +export const hasChromeExtensionInstalled = () => + 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 +const chromeWithoutProxy = (req, _store) => + new Promise((resolve, reject) => { + chrome.runtime.sendMessage( + EXTENSION_ID, + { + messageType: "send-req", + data: { + config: req + } + }, + ({ data }) => { + if (data.error) { + reject(data.error); + } else { + resolve(data.response); } } - }, (message) => { - if (message.data.error) { - reject(error); - } else { - resolve(message.data.response.data); + ); + }); + +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 + } + } + }, + ({ data }) => { + if (data.error) { + reject(error); + } else { + resolve(data.response.data); + } } - } - ) -}); + ); + }); const chromeStrategy = (req, store) => { if (store.state.postwoman.settings.PROXY_ENABLED) { @@ -51,6 +58,6 @@ const chromeStrategy = (req, store) => { } else { return chromeWithoutProxy(req, store); } -} +}; export default chromeStrategy; diff --git a/functions/strategies/ExtensionStrategy.js b/functions/strategies/ExtensionStrategy.js new file mode 100644 index 000000000..e12e167e8 --- /dev/null +++ b/functions/strategies/ExtensionStrategy.js @@ -0,0 +1,26 @@ +export const hasExtensionInstalled = () => + typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined"; + +const extensionWithProxy = async (req, { state }) => { + const { data } = await window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest({ + method: "post", + url: + state.postwoman.settings.PROXY_URL || "https://postwoman.apollotv.xyz/", + data: req + }); + return data; +}; + +const extensionWithoutProxy = async (req, _store) => { + const res = await window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest(req); + return res; +}; + +const extensionStrategy = (req, store) => { + if (store.state.postwoman.settings.PROXY_ENABLED) { + return extensionWithProxy(req, store); + } + return extensionWithoutProxy(req, store); +}; + +export default extensionStrategy; diff --git a/lang/en-US.js b/lang/en-US.js index eba422b4b..6986063e3 100644 --- a/lang/en-US.js +++ b/lang/en-US.js @@ -230,6 +230,7 @@ export default { payload: "Payload", choose_file: "Choose a file", file_imported: "File imported", + import_failed: "Import failed", f12_details: "(F12 for details)", we_use_cookies: "We use cookies", copied_to_clipboard: "Copied to clipboard", diff --git a/layouts/default.vue b/layouts/default.vue index 4302a71f8..ed40616ed 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -247,6 +247,11 @@ brush +
  • + + extensions + +
  • public @@ -408,7 +413,7 @@
    @@ -668,7 +673,7 @@