refactor: merge branch 'main' into refactor/monorepo

This commit is contained in:
Andrew Bastin
2021-09-16 22:24:21 +05:30
98 changed files with 3969 additions and 3007 deletions

View File

@@ -0,0 +1,68 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
jest: true,
},
parserOptions: {
sourceType: "module",
requireConfigFile: false,
},
extends: [
"@nuxtjs",
"@nuxtjs/eslint-config-typescript",
"prettier/prettier",
"eslint:recommended",
"plugin:vue/recommended",
"plugin:prettier/recommended",
"plugin:nuxt/recommended",
],
plugins: ["vue", "prettier"],
// add your custom rules here
rules: {
semi: [2, "never"],
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
"vue/max-attributes-per-line": "off",
"vue/component-name-in-template-casing": ["error", "PascalCase"],
"vue/html-self-closing": [
"error",
{
html: {
normal: "never",
void: "always",
},
},
],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline": "off",
"vue/require-default-prop": "warn",
"vue/require-prop-types": "warn",
"prettier/prettier": ["warn", { semi: false }],
"import/no-named-as-default": "off",
"no-undef": "off",
// localStorage block
"no-restricted-globals": [
"error",
{
name: "localStorage",
message:
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
},
],
// window.localStorage block
"no-restricted-syntax": [
"error",
{
selector: "CallExpression[callee.object.property.name='localStorage']",
message:
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
},
],
},
globals: {
$nuxt: true,
},
}

111
packages/hoppscotch-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,111 @@
# Created by .ignore support plugin (hsz.mobi)
# Firebase
.firebase
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# Mac OSX
.DS_Store
# Vim swap files
*.swp
# Build data
.hoppscotch
# File explorer
.directory
# Tests screenshots
tests/*/screenshots
# Tests videos
tests/*/videos
# Local Netlify folder
.netlify
# Andrew's crazy Volar shim generator
shims-volar.d.ts

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@@ -17,7 +17,7 @@
::-webkit-scrollbar-thumb {
@apply bg-divider bg-clip-content;
@apply rounded-full;
@apply border-solid border-4 border-transparent;
@apply border-solid border-transparent border-4;
@apply hover:(bg-dividerDark bg-clip-content);
}
@@ -36,8 +36,9 @@
}
input::placeholder,
textarea::placeholder {
@apply text-secondaryDark;
textarea::placeholder,
.CodeMirror-empty {
@apply text-secondary;
@apply opacity-25;
}
@@ -116,8 +117,8 @@ a {
&.link {
@apply items-center;
@apply px-1 py-0.5;
@apply -mx-1 -my-0.5;
@apply py-0.5 px-1;
@apply -my-0.5 -mx-1;
@apply text-accent;
@apply rounded;
@apply hover:text-accentDark;
@@ -198,7 +199,7 @@ hr {
.textarea {
@apply flex;
@apply w-full;
@apply px-4 py-2;
@apply py-2 px-4;
@apply bg-transparent;
@apply rounded;
@apply text-secondaryDark;
@@ -293,7 +294,7 @@ input[type="checkbox"] {
@apply cursor-pointer;
&::before {
@apply border-2 border-divider;
@apply border-divider border-2;
@apply rounded;
@apply inline-flex;
@apply items-center;
@@ -347,6 +348,7 @@ input[type="checkbox"] {
@apply justify-start;
@apply shadow;
@apply font-medium;
@apply transition;
font-size: var(--body-font-size);
line-height: var(--body-line-height);
@@ -358,7 +360,6 @@ input[type="checkbox"] {
@apply ml-auto;
@apply last:ml-4;
@apply sm:ml-8;
@apply transition;
@apply rounded;
@apply text-current;
@apply normal-case;
@@ -461,6 +462,32 @@ input[type="checkbox"] {
@apply w-full;
}
.CodeMirror {
@apply !h-auto;
font-size: var(--body-font-size);
&:not(.CodeMirror-focused) .CodeMirror-activeline-background {
background: transparent !important;
}
.CodeMirror-dialog-top {
@apply bg-primaryLight;
@apply border-dividerLight;
@apply px-4;
@apply py-2;
@apply z-5;
}
.CodeMirror-scroll {
@apply min-h-64;
}
* {
font-family: "Roboto Mono", monospace;
}
}
@media (max-width: 767px) {
main {
margin-bottom: env(safe-area-inset-bottom);

View File

@@ -1,5 +1,9 @@
<template>
<SmartModal v-if="show" :title="$t('collection.save_as')" @close="hideModal">
<SmartModal
v-if="show"
:title="$t('collection.save_as').toString()"
@close="hideModal"
>
<template #body>
<div class="flex flex-col px-2">
<div class="flex relative">
@@ -41,11 +45,11 @@
<template #footer>
<span>
<ButtonPrimary
:label="$t('action.save')"
:label="$t('action.save').toString()"
@click.native="saveRequestAs"
/>
<ButtonSecondary
:label="$t('action.cancel')"
:label="$t('action.cancel').toString()"
@click.native="hideModal"
/>
</span>
@@ -53,191 +57,294 @@
</SmartModal>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import * as teamUtils from "~/helpers/teams/utils"
<script setup lang="ts">
import { reactive, ref, useContext, watch } from "@nuxtjs/composition-api"
import { isHoppRESTRequest } from "~/helpers/types/HoppRESTRequest"
import {
saveRESTRequestAs,
editRESTRequest,
editGraphqlRequest,
editRESTRequest,
saveGraphqlRequestAs,
saveRESTRequestAs,
} from "~/newstore/collections"
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
import {
getRESTRequest,
useRESTRequestName,
setRESTSaveContext,
useRESTRequestName,
} from "~/newstore/RESTSession"
import * as teamUtils from "~/helpers/teams/utils"
import { apolloClient } from "~/helpers/apollo"
import { HoppGQLRequest } from "~/helpers/types/HoppGQLRequest"
export default defineComponent({
props: {
// mode can be either "graphql" or "rest"
mode: { type: String, default: "rest" },
show: Boolean,
},
setup(props) {
return {
requestName:
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName(),
type CollectionType =
| {
type: "my-collections"
}
},
data() {
return {
requestData: {
name: this.requestName,
collectionIndex: undefined,
folderName: undefined,
requestIndex: undefined,
},
collectionsType: {
type: "my-collections",
selectedTeam: undefined,
},
picked: null,
| {
type: "team-collections"
// TODO: Figure this type out
selectedTeam: {
id: string
}
}
},
watch: {
"requestData.collectionIndex": function resetFolderAndRequestIndex() {
// if user has chosen some folder, than selected other collection, which doesn't have any folders
// than `requestUpdateData.folderName` won't be reseted
this.$data.requestData.folderName = undefined
this.$data.requestData.requestIndex = undefined
},
"requestData.folderName": function resetRequestIndex() {
this.$data.requestData.requestIndex = undefined
},
editingRequest({ name }) {
this.$data.requestData.name = name || this.$data.defaultRequestName
},
},
methods: {
onUpdateCollType(newCollType) {
this.collectionsType = newCollType
},
onSelect({ picked }) {
this.picked = picked
},
async saveRequestAs() {
if (!this.requestName) {
this.$toast.error(this.$t("error.empty_req_name"), {
icon: "error_outline",
})
return
}
if (this.picked == null) {
this.$toast.error(this.$t("collection.select"), {
icon: "error_outline",
})
return
}
const requestUpdated =
this.mode === "rest" ? getRESTRequest() : getGQLSession()
type Picked =
| {
pickedType: "my-request"
folderPath: string
requestIndex: number
}
| {
pickedType: "my-folder"
folderPath: string
}
| {
pickedType: "my-collection"
collectionIndex: number
}
| {
pickedType: "teams-request"
requestID: string
}
| {
pickedType: "teams-folder"
folderID: string
}
| {
pickedType: "teams-collection"
collectionID: string
}
| {
pickedType: "gql-my-request"
folderPath: string
requestIndex: number
}
| {
pickedType: "gql-my-folder"
folderPath: string
}
| {
pickedType: "gql-my-collection"
collectionIndex: number
}
// Filter out all REST file inputs
if (this.mode === "rest" && requestUpdated.bodyParams) {
requestUpdated.bodyParams = requestUpdated.bodyParams.map((param) =>
param?.value?.[0] instanceof File ? { ...param, value: "" } : param
)
}
const props = defineProps<{
mode: "rest" | "graphql"
show: boolean
}>()
if (this.picked.pickedType === "my-request") {
editRESTRequest(
this.picked.folderPath,
this.picked.requestIndex,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: this.picked.folderPath,
requestIndex: this.picked.requestIndex,
})
} else if (this.picked.pickedType === "my-folder") {
const insertionIndex = saveRESTRequestAs(
this.picked.folderPath,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: this.picked.folderPath,
requestIndex: insertionIndex,
})
} else if (this.picked.pickedType === "my-collection") {
const insertionIndex = saveRESTRequestAs(
`${this.picked.collectionIndex}`,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: `${this.picked.collectionIndex}`,
requestIndex: insertionIndex,
})
} else if (this.picked.pickedType === "teams-request") {
teamUtils.overwriteRequestTeams(
this.$apollo,
JSON.stringify(requestUpdated),
requestUpdated.name,
this.picked.requestID
)
setRESTSaveContext({
originLocation: "team-collection",
requestID: this.picked.requestID,
})
} else if (this.picked.pickedType === "teams-folder") {
const req = await teamUtils.saveRequestAsTeams(
this.$apollo,
JSON.stringify(requestUpdated),
requestUpdated.name,
this.collectionsType.selectedTeam.id,
this.picked.folderID
)
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
if (req && req.id) {
setRESTSaveContext({
originLocation: "team-collection",
requestID: req.id,
teamID: this.collectionsType.selectedTeam.id,
collectionID: this.picked.folderID,
})
}
} else if (this.picked.pickedType === "teams-collection") {
const req = await teamUtils.saveRequestAsTeams(
this.$apollo,
JSON.stringify(requestUpdated),
requestUpdated.name,
this.collectionsType.selectedTeam.id,
this.picked.collectionID
)
const {
$toast,
app: { i18n },
} = useContext()
if (req && req.id) {
setRESTSaveContext({
originLocation: "team-collection",
requestID: req.id,
teamID: this.collectionsType.selectedTeam.id,
collectionID: this.picked.collectionID,
})
}
} else if (this.picked.pickedType === "gql-my-request") {
editGraphqlRequest(
this.picked.folderPath,
this.picked.requestIndex,
requestUpdated
)
} else if (this.picked.pickedType === "gql-my-folder") {
saveGraphqlRequestAs(this.picked.folderPath, requestUpdated)
} else if (this.picked.pickedType === "gql-my-collection") {
saveGraphqlRequestAs(`${this.picked.collectionIndex}`, requestUpdated)
}
this.$toast.success(this.$t("request.added"), {
icon: "post_add",
})
const t = i18n.t.bind(i18n)
this.hideModal()
},
hideModal() {
this.picked = null
this.$emit("hide-modal")
},
},
// TODO: Use a better implementation with computed ?
// This implementation can't work across updates to mode prop (which won't happen tho)
const requestName =
props.mode === "rest" ? useRESTRequestName() : useGQLRequestName()
const requestData = reactive({
name: requestName,
collectionIndex: undefined as number | undefined,
folderName: undefined as number | undefined,
requestIndex: undefined as number | undefined,
})
const collectionsType = ref<CollectionType>({
type: "my-collections",
})
// TODO: Figure this type out
const picked = ref<Picked | null>(null)
// Resets
watch(
() => requestData.collectionIndex,
() => {
requestData.folderName = undefined
requestData.requestIndex = undefined
}
)
watch(
() => requestData.folderName,
() => {
requestData.requestIndex = undefined
}
)
// All the methods
const onUpdateCollType = (newCollType: CollectionType) => {
collectionsType.value = newCollType
}
const onSelect = ({ picked: pickedVal }: { picked: Picked | null }) => {
picked.value = pickedVal
}
const hideModal = () => {
picked.value = null
emit("hide-modal")
}
const saveRequestAs = async () => {
if (!requestName.value) {
$toast.error(t("error.empty_req_name").toString(), {
icon: "error_outline",
})
return
}
if (picked.value === null) {
$toast.error(t("collection.select").toString(), {
icon: "error_outline",
})
return
}
const requestUpdated =
props.mode === "rest" ? getRESTRequest() : getGQLSession().request
// // Filter out all REST file inputs
// if (this.mode === "rest" && requestUpdated.bodyParams) {
// requestUpdated.bodyParams = requestUpdated.bodyParams.map((param) =>
// param?.value?.[0] instanceof File ? { ...param, value: "" } : param
// )
// }
if (picked.value.pickedType === "my-request") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
editRESTRequest(
picked.value.folderPath,
picked.value.requestIndex,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: picked.value.folderPath,
requestIndex: picked.value.requestIndex,
})
} else if (picked.value.pickedType === "my-folder") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
const insertionIndex = saveRESTRequestAs(
picked.value.folderPath,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: picked.value.folderPath,
requestIndex: insertionIndex,
})
} else if (picked.value.pickedType === "my-collection") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
const insertionIndex = saveRESTRequestAs(
`${picked.value.collectionIndex}`,
requestUpdated
)
setRESTSaveContext({
originLocation: "user-collection",
folderPath: `${picked.value.collectionIndex}`,
requestIndex: insertionIndex,
})
} else if (picked.value.pickedType === "teams-request") {
if (isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
if (collectionsType.value.type !== "team-collections")
throw new Error("Collections Type mismatch")
teamUtils.overwriteRequestTeams(
apolloClient,
JSON.stringify(requestUpdated),
requestUpdated.name,
picked.value.requestID
)
setRESTSaveContext({
originLocation: "team-collection",
requestID: picked.value.requestID,
})
} else if (picked.value.pickedType === "teams-folder") {
if (isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
if (collectionsType.value.type !== "team-collections")
throw new Error("Collections Type mismatch")
const req = await teamUtils.saveRequestAsTeams(
apolloClient,
JSON.stringify(requestUpdated),
requestUpdated.name,
collectionsType.value.selectedTeam.id,
picked.value.folderID
)
if (req && req.id) {
setRESTSaveContext({
originLocation: "team-collection",
requestID: req.id,
teamID: collectionsType.value.selectedTeam.id,
collectionID: picked.value.folderID,
})
}
} else if (picked.value.pickedType === "teams-collection") {
if (isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
if (collectionsType.value.type !== "team-collections")
throw new Error("Collections Type mismatch")
const req = await teamUtils.saveRequestAsTeams(
apolloClient,
JSON.stringify(requestUpdated),
requestUpdated.name,
collectionsType.value.selectedTeam.id,
picked.value.collectionID
)
if (req && req.id) {
setRESTSaveContext({
originLocation: "team-collection",
requestID: req.id,
teamID: collectionsType.value.selectedTeam.id,
collectionID: picked.value.collectionID,
})
}
} else if (picked.value.pickedType === "gql-my-request") {
// TODO: Check for GQL request ?
editGraphqlRequest(
picked.value.folderPath,
picked.value.requestIndex,
requestUpdated as HoppGQLRequest
)
} else if (picked.value.pickedType === "gql-my-folder") {
// TODO: Check for GQL request ?
saveGraphqlRequestAs(
picked.value.folderPath,
requestUpdated as HoppGQLRequest
)
} else if (picked.value.pickedType === "gql-my-collection") {
// TODO: Check for GQL request ?
saveGraphqlRequestAs(
`${picked.value.collectionIndex}`,
requestUpdated as HoppGQLRequest
)
}
$toast.success(t("request.added").toString(), {
icon: "post_add",
})
hideModal()
}
</script>

View File

@@ -26,7 +26,7 @@
/>
</div>
<div v-if="mode === 'email'" class="flex flex-col space-y-2">
<div class="flex items-center relative">
<div class="flex flex-col">
<input
id="email"
v-model="form.email"

View File

@@ -1,254 +0,0 @@
<template>
<div class="opacity-0 show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import "ace-builds/src-noconflict/ext-language_tools"
import "ace-builds/src-noconflict/mode-graphqlschema"
import * as gql from "graphql"
import { getAutocompleteSuggestions } from "graphql-language-service-interface"
import { defineComponent } from "@nuxtjs/composition-api"
import { defineGQLLanguageMode } from "~/helpers/syntax/gqlQueryLangMode"
import debounce from "~/helpers/utils/debounce"
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
onRunGQLQuery: {
type: Function,
default: () => {},
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
validationSchema: null,
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
defineGQLLanguageMode(ace)
const langTools = ace.require("ace/ext/language_tools")
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/gql-query`,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
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
this.cacheValue = this.value
editor.commands.addCommand({
name: "runGQLQuery",
exec: () => this.onRunGQLQuery(this.editor.getValue()),
bindKey: {
mac: "cmd-enter",
win: "ctrl-enter",
},
})
editor.commands.addCommand({
name: "prettifyGQLQuery",
exec: () => this.prettifyQuery(),
bindKey: {
mac: "cmd-p",
win: "ctrl-p",
},
})
editor.on("change", () => {
const content = editor.getValue()
this.$emit("input", content)
this.parseContents(content)
this.cacheValue = content
})
this.parseContents(this.value)
},
beforeDestroy() {
this.editor.destroy()
},
methods: {
prettifyQuery() {
try {
this.$emit("update-query", gql.print(gql.parse(this.editor.getValue())))
} catch (e) {
this.$toast.error(this.$t("error.gql_prettify_invalid_query"), {
icon: "error_outline",
})
}
},
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-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(({ locations, message }) => ({
row: locations[0].line - 1,
column: locations[0].column - 1,
text: 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),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -33,45 +33,32 @@
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { GQLConnection } from "~/helpers/GQLConnection"
import { getCurrentStrategyID } from "~/helpers/network"
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup(props) {
const connected = useReadonlyStream(props.conn.connected$, false)
const headers = useReadonlyStream(gqlHeaders$, [])
const props = defineProps<{
conn: GQLConnection
}>()
const url = useStream(gqlURL$, "", setGQLURL)
const connected = useReadonlyStream(props.conn.connected$, false)
const headers = useReadonlyStream(gqlHeaders$, [])
const onConnectClick = () => {
if (!connected.value) {
props.conn.connect(url.value, headers.value as any)
const url = useStream(gqlURL$, "", setGQLURL)
logHoppRequestRunToAnalytics({
platform: "graphql-schema",
strategy: getCurrentStrategyID(),
})
} else {
props.conn.disconnect()
}
}
const onConnectClick = () => {
if (!connected.value) {
props.conn.connect(url.value, headers.value as any)
return {
url,
connected,
onConnectClick,
}
},
})
logHoppRequestRunToAnalytics({
platform: "graphql-schema",
strategy: getCurrentStrategyID(),
})
} else {
props.conn.disconnect()
}
}
</script>

View File

@@ -42,9 +42,7 @@
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="`${$t(
'action.prettify'
)} <kbd>${getSpecialKey()}</kbd><kbd>P</kbd>`"
:title="$t('action.prettify')"
:svg="prettifyQueryIcon"
@click.native="prettifyQuery"
/>
@@ -57,20 +55,7 @@
/>
</div>
</div>
<GraphqlQueryEditor
ref="queryEditor"
v-model="gqlQueryString"
:on-run-g-q-l-query="runQuery"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
@update-query="updateQuery"
/>
<div ref="queryEditor"></div>
</AppSection>
</SmartTab>
@@ -108,19 +93,7 @@
/>
</div>
</div>
<SmartAceEditor
ref="variableEditor"
v-model="variableString"
:lang="'json'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div ref="variableEditor"></div>
</AppSection>
</SmartTab>
@@ -173,27 +146,7 @@
/>
</div>
</div>
<div v-if="bulkMode" class="flex">
<textarea-autosize
v-model="bulkHeaders"
v-focus
name="bulk-parameters"
class="
bg-transparent
border-b border-dividerLight
flex
font-mono
flex-1
py-2
px-4
whitespace-pre
resize-y
overflow-auto
"
rows="10"
:placeholder="$t('state.bulk_mode_placeholder')"
/>
</div>
<div v-if="bulkMode" ref="bulkEditor"></div>
<div v-else>
<div
v-for="(header, index) in headers"
@@ -229,7 +182,9 @@
/>
<input
class="bg-transparent flex flex-1 py-2 px-4"
:placeholder="$t('count.value', { count: index + 1 })"
:placeholder="
$t('count.value', { count: index + 1 }).toString()
"
:name="`value ${index}`"
:value="header.value"
autofocus
@@ -311,17 +266,10 @@
</div>
</template>
<script lang="ts">
import {
defineComponent,
onMounted,
PropType,
ref,
useContext,
watch,
} from "@nuxtjs/composition-api"
<script setup lang="ts">
import { onMounted, ref, useContext, watch } from "@nuxtjs/composition-api"
import clone from "lodash/clone"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import * as gql from "graphql"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import {
useNuxt,
@@ -348,208 +296,207 @@ import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { getCurrentStrategyID } from "~/helpers/network"
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
import { useCodemirror } from "~/helpers/editor/codemirror"
import "codemirror/mode/javascript/javascript"
import "~/helpers/editor/modes/graphql"
import jsonLinter from "~/helpers/editor/linting/json"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup(props) {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const nuxt = useNuxt()
const props = defineProps<{
conn: GQLConnection
}>()
const bulkMode = ref(false)
const bulkHeaders = ref("")
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const nuxt = useNuxt()
watch(bulkHeaders, () => {
try {
const transformation = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setGQLHeaders(transformation)
} catch (e) {
$toast.error(t("error.something_went_wrong").toString(), {
icon: "error_outline",
})
console.error(e)
}
const bulkMode = ref(false)
const bulkHeaders = ref("")
watch(bulkHeaders, () => {
try {
const transformation = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setGQLHeaders(transformation)
} catch (e) {
$toast.error(t("error.something_went_wrong").toString(), {
icon: "error_outline",
})
console.error(e)
}
})
const url = useReadonlyStream(gqlURL$, "")
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
const variableString = useStream(gqlVariables$, "", setGQLVariables)
const headers = useStream(gqlHeaders$, [], setGQLHeaders)
const url = useReadonlyStream(gqlURL$, "")
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
const variableString = useStream(gqlVariables$, "", setGQLVariables)
const headers = useStream(gqlHeaders$, [], setGQLHeaders)
const queryEditor = ref<any | null>(null)
const bulkEditor = ref<any | null>(null)
const copyQueryIcon = ref("copy")
const prettifyQueryIcon = ref("align-left")
const copyVariablesIcon = ref("copy")
useCodemirror(bulkEditor, bulkHeaders, {
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: t("state.bulk_mode_placeholder").toString(),
},
linter: null,
completer: null,
})
const showSaveRequestModal = ref(false)
const variableEditor = ref<any | null>(null)
const schema = useReadonlyStream(props.conn.schemaString$, "")
useCodemirror(variableEditor, variableString, {
extendedEditorConfig: {
mode: "application/ld+json",
placeholder: t("request.variables").toString(),
},
linter: jsonLinter,
completer: null,
})
watch(
headers,
() => {
if (
(headers.value[headers.value.length - 1]?.key !== "" ||
headers.value[headers.value.length - 1]?.value !== "") &&
headers.value.length
)
addRequestHeader()
},
{ deep: true }
const queryEditor = ref<any | null>(null)
const schemaString = useReadonlyStream(props.conn.schema$, null)
useCodemirror(queryEditor, gqlQueryString, {
extendedEditorConfig: {
mode: "graphql",
placeholder: t("request.query").toString(),
},
linter: createGQLQueryLinter(schemaString),
completer: queryCompleter(schemaString),
})
const copyQueryIcon = ref("copy")
const prettifyQueryIcon = ref("align-left")
const copyVariablesIcon = ref("copy")
const showSaveRequestModal = ref(false)
watch(
headers,
() => {
if (
(headers.value[headers.value.length - 1]?.key !== "" ||
headers.value[headers.value.length - 1]?.value !== "") &&
headers.value.length
)
addRequestHeader()
},
{ deep: true }
)
onMounted(() => {
if (!headers.value?.length) {
addRequestHeader()
}
})
const copyQuery = () => {
copyToClipboard(gqlQueryString.value)
copyQueryIcon.value = "check"
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
}
const response = useStream(gqlResponse$, "", setGQLResponse)
const runQuery = async () => {
const startTime = Date.now()
nuxt.value.$loading.start()
response.value = t("state.loading").toString()
try {
const runURL = clone(url.value)
const runHeaders = clone(headers.value)
const runQuery = clone(gqlQueryString.value)
const runVariables = clone(variableString.value)
const responseText = await props.conn.runQuery(
runURL,
runHeaders,
runQuery,
runVariables
)
const duration = Date.now() - startTime
nuxt.value.$loading.finish()
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
addGraphqlHistoryEntry(
makeGQLHistoryEntry({
request: makeGQLRequest({
name: "",
url: runURL,
query: runQuery,
headers: runHeaders,
variables: runVariables,
}),
response: response.value,
star: false,
})
)
onMounted(() => {
if (!headers.value?.length) {
addRequestHeader()
}
$toast.success(t("state.finished_in", { duration }).toString(), {
icon: "done",
})
} catch (e: any) {
response.value = `${e}. ${t("error.check_console_details")}`
nuxt.value.$loading.finish()
const copyQuery = () => {
copyToClipboard(gqlQueryString.value)
copyQueryIcon.value = "check"
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
}
$toast.error(`${e} ${t("error.f12_details").toString()}`, {
icon: "error_outline",
})
console.error(e)
}
const response = useStream(gqlResponse$, "", setGQLResponse)
logHoppRequestRunToAnalytics({
platform: "graphql-query",
strategy: getCurrentStrategyID(),
})
}
const runQuery = async () => {
const startTime = Date.now()
const hideRequestModal = () => {
showSaveRequestModal.value = false
}
nuxt.value.$loading.start()
response.value = t("state.loading").toString()
const prettifyQuery = () => {
try {
gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
} catch (e) {
$toast.error(t("error.gql_prettify_invalid_query").toString(), {
icon: "error_outline",
})
}
prettifyQueryIcon.value = "check"
setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000)
}
try {
const runURL = clone(url.value)
const runHeaders = clone(headers.value)
const runQuery = clone(gqlQueryString.value)
const runVariables = clone(variableString.value)
const saveRequest = () => {
showSaveRequestModal.value = true
}
const responseText = await props.conn.runQuery(
runURL,
runHeaders,
runQuery,
runVariables
)
const duration = Date.now() - startTime
const copyVariables = () => {
copyToClipboard(variableString.value)
copyVariablesIcon.value = "check"
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
}
nuxt.value.$loading.finish()
const addRequestHeader = () => {
addGQLHeader({
key: "",
value: "",
active: true,
})
}
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
addGraphqlHistoryEntry(
makeGQLHistoryEntry({
request: makeGQLRequest({
name: "",
url: runURL,
query: runQuery,
headers: runHeaders,
variables: runVariables,
}),
response: response.value,
star: false,
})
)
$toast.success(t("state.finished_in", { duration }).toString(), {
icon: "done",
})
} catch (e: any) {
response.value = `${e}. ${t("error.check_console_details")}`
nuxt.value.$loading.finish()
$toast.error(`${e} ${t("error.f12_details").toString()}`, {
icon: "error_outline",
})
console.error(e)
}
logHoppRequestRunToAnalytics({
platform: "graphql-query",
strategy: getCurrentStrategyID(),
})
}
const hideRequestModal = () => {
showSaveRequestModal.value = false
}
const prettifyQuery = () => {
queryEditor.value.prettifyQuery()
prettifyQueryIcon.value = "check"
setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000)
}
const saveRequest = () => {
showSaveRequestModal.value = true
}
// Why ?
const updateQuery = (updatedQuery: string) => {
gqlQueryString.value = updatedQuery
}
const copyVariables = () => {
copyToClipboard(variableString.value)
copyVariablesIcon.value = "check"
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
}
const addRequestHeader = () => {
addGQLHeader({
key: "",
value: "",
active: true,
})
}
const removeRequestHeader = (index: number) => {
removeGQLHeader(index)
}
return {
gqlQueryString,
variableString,
headers,
copyQueryIcon,
prettifyQueryIcon,
copyVariablesIcon,
queryEditor,
showSaveRequestModal,
hideRequestModal,
schema,
copyQuery,
runQuery,
prettifyQuery,
saveRequest,
updateQuery,
copyVariables,
addRequestHeader,
removeRequestHeader,
getSpecialKey: getPlatformSpecialKey,
commonHeaders,
updateGQLHeader,
bulkMode,
bulkHeaders,
}
},
})
const removeRequestHeader = (index: number) => {
removeGQLHeader(index)
}
</script>

View File

@@ -18,6 +18,13 @@
{{ $t("response.title") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
ref="downloadResponse"
v-tippy="{ theme: 'tooltip' }"
@@ -34,21 +41,7 @@
/>
</div>
</div>
<SmartAceEditor
v-if="responseString"
:value="responseString"
:lang="'json'"
:lint="false"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div v-if="responseString" ref="schemaEditor"></div>
<div
v-else
class="
@@ -60,35 +53,21 @@
"
>
<div class="flex space-x-2 pb-4">
<div class="flex flex-col space-y-4 items-end">
<div class="flex flex-col space-y-4 text-right items-end">
<span class="flex flex-1 items-center">
{{ $t("shortcut.request.send_request") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.show_all") }}
</span>
<!-- <span class="flex flex-1 items-center">
{{ $t("shortcut.general.command_menu") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.help_menu") }}
</span> -->
</span>
</div>
<div class="flex flex-col space-y-4">
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">G</span>
</div>
<div class="flex">
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">K</span>
</div>
<!-- <div class="flex">
<span class="shortcut-key">/</span>
</div>
<div class="flex">
<span class="shortcut-key">?</span>
</div> -->
</div>
</div>
</div>
<ButtonSecondary
@@ -103,77 +82,66 @@
</AppSection>
</template>
<script lang="ts">
import {
defineComponent,
PropType,
ref,
useContext,
} from "@nuxtjs/composition-api"
import { GQLConnection } from "~/helpers/GQLConnection"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
<script setup lang="ts">
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { gqlResponse$ } from "~/newstore/GQLSession"
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const responseString = useReadonlyStream(gqlResponse$, "")
const schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
schemaEditor,
responseString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
readOnly: true,
lineWrapping: linewrapEnabled,
},
},
setup() {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
linter: null,
completer: null,
})
)
const responseString = useReadonlyStream(gqlResponse$, "")
const downloadResponseIcon = ref("download")
const copyResponseIcon = ref("copy")
const downloadResponseIcon = ref("download")
const copyResponseIcon = ref("copy")
const copyResponse = () => {
copyToClipboard(responseString.value!)
copyResponseIcon.value = "check"
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
}
const copyResponse = () => {
copyToClipboard(responseString.value!)
copyResponseIcon.value = "check"
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
}
const downloadResponse = () => {
const dataToWrite = responseString.value
const file = new Blob([dataToWrite!], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadResponseIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadResponseIcon.value = "download"
}, 1000)
}
return {
responseString,
downloadResponseIcon,
copyResponseIcon,
downloadResponse,
copyResponse,
getSpecialKey: getPlatformSpecialKey,
}
},
})
const downloadResponse = () => {
const dataToWrite = responseString.value
const file = new Blob([dataToWrite!], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadResponseIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadResponseIcon.value = "download"
}, 1000)
}
</script>
<style lang="scss" scoped>

View File

@@ -149,6 +149,13 @@
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
ref="downloadSchema"
v-tippy="{ theme: 'tooltip' }"
@@ -165,20 +172,7 @@
/>
</div>
</div>
<SmartAceEditor
v-if="schemaString"
v-model="schemaString"
:lang="'graphqlschema'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div v-if="schemaString" ref="schemaEditor"></div>
<div
v-else
class="
@@ -200,17 +194,17 @@
</aside>
</template>
<script lang="ts">
<script setup lang="ts">
import {
computed,
defineComponent,
nextTick,
PropType,
reactive,
ref,
useContext,
} from "@nuxtjs/composition-api"
import { GraphQLField, GraphQLType } from "graphql"
import { map } from "rxjs/operators"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { GQLConnection } from "~/helpers/GQLConnection"
import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
import { copyToClipboard } from "~/helpers/utils/clipboard"
@@ -222,6 +216,7 @@ import {
setGQLURL,
setGQLVariables,
} from "~/newstore/GQLSession"
import "~/helpers/editor/modes/graphql"
function isTextFoundInGraphqlFieldObject(
text: string,
@@ -285,186 +280,168 @@ type GQLHistoryEntry = {
variables: string
}
export default defineComponent({
props: {
conn: {
type: Object as PropType<GQLConnection>,
required: true,
},
},
setup(props) {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const props = defineProps<{
conn: GQLConnection
}>()
const queryFields = useReadonlyStream(
props.conn.queryFields$.pipe(map((x) => x ?? [])),
[]
)
const mutationFields = useReadonlyStream(
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
[]
)
const subscriptionFields = useReadonlyStream(
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
[]
)
const graphqlTypes = useReadonlyStream(
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
[]
)
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const downloadSchemaIcon = ref("download")
const copySchemaIcon = ref("copy")
const queryFields = useReadonlyStream(
props.conn.queryFields$.pipe(map((x) => x ?? [])),
[]
)
const graphqlFieldsFilterText = ref("")
const mutationFields = useReadonlyStream(
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
[]
)
const gqlTabs = ref<any | null>(null)
const typesTab = ref<any | null>(null)
const subscriptionFields = useReadonlyStream(
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
[]
)
const filteredQueryFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
queryFields.value as any
)
})
const graphqlTypes = useReadonlyStream(
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
[]
)
const filteredMutationFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
mutationFields.value as any
)
})
const downloadSchemaIcon = ref("download")
const copySchemaIcon = ref("copy")
const filteredSubscriptionFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
subscriptionFields.value as any
)
})
const graphqlFieldsFilterText = ref("")
const filteredGraphqlTypes = computed(() => {
return getFilteredGraphqlTypes(
graphqlFieldsFilterText.value,
graphqlTypes.value as any
)
})
const gqlTabs = ref<any | null>(null)
const typesTab = ref<any | null>(null)
const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
if (!graphqlFieldsFilterText.value) return false
return isTextFoundInGraphqlFieldObject(
graphqlFieldsFilterText.value,
gqlType as any
)
}
const getGqlTypeHighlightedFields = (gqlType: GraphQLType) => {
if (!graphqlFieldsFilterText.value) return []
const fields = Object.values((gqlType as any)._fields || {})
if (!fields || fields.length === 0) return []
return fields.filter((field) =>
isTextFoundInGraphqlFieldObject(
graphqlFieldsFilterText.value,
field as any
)
)
}
const handleJumpToType = async (type: GraphQLType) => {
gqlTabs.value.selectTab(typesTab.value)
await nextTick()
const rootTypeName = resolveRootType(type).name
const target = document.getElementById(`type_${rootTypeName}`)
if (target) {
gqlTabs.value.$el
.querySelector(".gqlTabs")
.scrollTo({ top: target.offsetTop, behavior: "smooth" })
}
}
const schemaString = useReadonlyStream(
props.conn.schemaString$.pipe(map((x) => x ?? "")),
""
)
const downloadSchema = () => {
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
const file = new Blob([dataToWrite], { type: "application/graphql" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${
url.split("/").pop()!.split("#")[0].split("?")[0]
}.graphql`
document.body.appendChild(a)
a.click()
downloadSchemaIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadSchemaIcon.value = "download"
}, 1000)
}
const copySchema = () => {
if (!schemaString.value) return
copyToClipboard(schemaString.value)
copySchemaIcon.value = "check"
setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
}
const handleUseHistory = (entry: GQLHistoryEntry) => {
const url = entry.url
const headers = entry.headers
const gqlQueryString = entry.query
const variableString = entry.variables
const responseText = entry.response
setGQLURL(url)
setGQLHeaders(headers)
setGQLQuery(gqlQueryString)
setGQLVariables(variableString)
setGQLResponse(responseText)
props.conn.reset()
}
return {
queryFields,
mutationFields,
subscriptionFields,
graphqlTypes,
schemaString,
graphqlFieldsFilterText,
filteredQueryFields,
filteredMutationFields,
filteredSubscriptionFields,
filteredGraphqlTypes,
isGqlTypeHighlighted,
getGqlTypeHighlightedFields,
gqlTabs,
typesTab,
handleJumpToType,
downloadSchema,
downloadSchemaIcon,
copySchemaIcon,
copySchema,
handleUseHistory,
}
},
const filteredQueryFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
queryFields.value as any
)
})
const filteredMutationFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
mutationFields.value as any
)
})
const filteredSubscriptionFields = computed(() => {
return getFilteredGraphqlFields(
graphqlFieldsFilterText.value,
subscriptionFields.value as any
)
})
const filteredGraphqlTypes = computed(() => {
return getFilteredGraphqlTypes(
graphqlFieldsFilterText.value,
graphqlTypes.value as any
)
})
const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
if (!graphqlFieldsFilterText.value) return false
return isTextFoundInGraphqlFieldObject(
graphqlFieldsFilterText.value,
gqlType as any
)
}
const getGqlTypeHighlightedFields = (gqlType: GraphQLType) => {
if (!graphqlFieldsFilterText.value) return []
const fields = Object.values((gqlType as any)._fields || {})
if (!fields || fields.length === 0) return []
return fields.filter((field) =>
isTextFoundInGraphqlFieldObject(graphqlFieldsFilterText.value, field as any)
)
}
const handleJumpToType = async (type: GraphQLType) => {
gqlTabs.value.selectTab(typesTab.value)
await nextTick()
const rootTypeName = resolveRootType(type).name
const target = document.getElementById(`type_${rootTypeName}`)
if (target) {
gqlTabs.value.$el
.querySelector(".gqlTabs")
.scrollTo({ top: target.offsetTop, behavior: "smooth" })
}
}
const schemaString = useReadonlyStream(
props.conn.schemaString$.pipe(map((x) => x ?? "")),
""
)
const schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
schemaEditor,
schemaString,
reactive({
extendedEditorConfig: {
mode: "graphql",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
})
)
const downloadSchema = () => {
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
const file = new Blob([dataToWrite], { type: "application/graphql" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.graphql`
document.body.appendChild(a)
a.click()
downloadSchemaIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadSchemaIcon.value = "download"
}, 1000)
}
const copySchema = () => {
if (!schemaString.value) return
copyToClipboard(schemaString.value)
copySchemaIcon.value = "check"
setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
}
const handleUseHistory = (entry: GQLHistoryEntry) => {
const url = entry.url
const headers = entry.headers
const gqlQueryString = entry.query
const variableString = entry.variables
const responseText = entry.response
setGQLURL(url)
setGQLHeaders(headers)
setGQLQuery(gqlQueryString)
setGQLVariables(variableString)
setGQLResponse(responseText)
props.conn.reset()
}
</script>

View File

@@ -38,31 +38,25 @@
{{ t("request.generated_code") }}
</label>
</div>
<SmartAceEditor
<div
v-if="codegenType"
ref="generatedCode"
:value="requestCode"
:lang="codegens.find((x) => x.id === codegenType).language"
:options="{
maxLines: 16,
minLines: 8,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border rounded border-dividerLight"
/>
class="border border-dividerLight rounded"
></div>
</div>
</template>
<template #footer>
<ButtonPrimary
ref="copyRequestCode"
:label="t('action.copy')"
:svg="copyIcon"
@click.native="copyRequestCode"
/>
<ButtonSecondary :label="t('action.dismiss')" @click.native="hideModal" />
<span class="flex">
<ButtonPrimary
:label="t('action.copy').toString()"
:svg="copyIcon"
@click.native="copyRequestCode"
/>
<ButtonSecondary
:label="t('action.dismiss').toString()"
@click.native="hideModal"
/>
</span>
</template>
</SmartModal>
</template>
@@ -70,6 +64,7 @@
<script setup lang="ts">
import { computed, ref, useContext, watch } from "@nuxtjs/composition-api"
import { codegens, generateCodegenContext } from "~/helpers/codegen/codegen"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { getEffectiveRESTRequest } from "~/helpers/utils/EffectiveURL"
import { getCurrentEnvironment } from "~/newstore/environments"
@@ -106,6 +101,17 @@ const requestCode = computed(() => {
.generator(generateCodegenContext(effectiveRequest))
})
const generatedCode = ref<any | null>(null)
useCodemirror(generatedCode, requestCode, {
extendedEditorConfig: {
mode: "text/plain",
readOnly: true,
},
linter: null,
completer: null,
})
watch(
() => props.show,
(goingToShow) => {

View File

@@ -47,27 +47,7 @@
/>
</div>
</div>
<div v-if="bulkMode" class="flex">
<textarea-autosize
v-model="bulkHeaders"
v-focus
name="bulk-headers"
class="
bg-transparent
border-b border-dividerLight
flex
font-mono
flex-1
py-2
px-4
whitespace-pre
resize-y
overflow-auto
"
rows="10"
:placeholder="$t('state.bulk_mode_placeholder')"
/>
</div>
<div v-if="bulkMode" ref="bulkEditor"></div>
<div v-else>
<div
v-for="(header, index) in headers$"
@@ -193,96 +173,86 @@
</AppSection>
</template>
<script lang="ts">
<script setup lang="ts">
import { ref, useContext, watch } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import {
defineComponent,
ref,
useContext,
watch,
} from "@nuxtjs/composition-api"
import {
restHeaders$,
addRESTHeader,
updateRESTHeader,
deleteRESTHeader,
deleteAllRESTHeaders,
deleteRESTHeader,
restHeaders$,
setRESTHeaders,
updateRESTHeader,
} from "~/newstore/RESTSession"
import { commonHeaders } from "~/helpers/headers"
import { useSetting } from "~/newstore/settings"
import { useReadonlyStream } from "~/helpers/utils/composables"
import { HoppRESTHeader } from "~/helpers/types/HoppRESTRequest"
export default defineComponent({
setup() {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const bulkMode = ref(false)
const bulkHeaders = ref("")
const bulkMode = ref(false)
const bulkHeaders = ref("")
const bulkEditor = ref<any | null>(null)
watch(bulkHeaders, () => {
try {
const transformation = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setRESTHeaders(transformation)
} catch (e) {
$toast.error(t("error.something_went_wrong").toString(), {
icon: "error_outline",
})
console.error(e)
}
})
return {
headers$: useReadonlyStream(restHeaders$, []),
EXPERIMENTAL_URL_BAR_ENABLED: useSetting("EXPERIMENTAL_URL_BAR_ENABLED"),
bulkMode,
bulkHeaders,
}
},
data() {
return {
commonHeaders,
}
},
watch: {
headers$: {
handler(newValue) {
if (
(newValue[newValue.length - 1]?.key !== "" ||
newValue[newValue.length - 1]?.value !== "") &&
newValue.length
)
this.addHeader()
},
deep: true,
},
},
// mounted() {
// if (!this.headers$?.length) {
// this.addHeader()
// }
// },
methods: {
addHeader() {
addRESTHeader({ key: "", value: "", active: true })
},
updateHeader(index: number, item: HoppRESTHeader) {
updateRESTHeader(index, item)
},
deleteHeader(index: number) {
deleteRESTHeader(index)
},
clearContent() {
deleteAllRESTHeaders()
},
useCodemirror(bulkEditor, bulkHeaders, {
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: t("state.bulk_mode_placeholder").toString(),
},
linter: null,
completer: null,
})
watch(bulkHeaders, () => {
try {
const transformation = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setRESTHeaders(transformation)
} catch (e) {
$toast.error(t("error.something_went_wrong").toString(), {
icon: "error_outline",
})
console.error(e)
}
})
const headers$ = useReadonlyStream(restHeaders$, [])
watch(
headers$,
(newValue) => {
if (
(newValue[newValue.length - 1]?.key !== "" ||
newValue[newValue.length - 1]?.value !== "") &&
newValue.length
)
addHeader()
},
{ deep: true }
)
const addHeader = () => {
addRESTHeader({ key: "", value: "", active: true })
}
const updateHeader = (index: number, item: HoppRESTHeader) => {
updateRESTHeader(index, item)
}
const deleteHeader = (index: number) => {
deleteRESTHeader(index)
}
const clearContent = () => {
deleteAllRESTHeaders()
}
const EXPERIMENTAL_URL_BAR_ENABLED = useSetting("EXPERIMENTAL_URL_BAR_ENABLED")
</script>

View File

@@ -1,28 +1,22 @@
<template>
<SmartModal v-if="show" :title="$t('import.curl')" @close="hideModal">
<SmartModal
v-if="show"
:title="$t('import.curl').toString()"
@close="hideModal"
>
<template #body>
<div class="flex flex-col px-2">
<textarea-autosize
id="import-curl"
v-model="curl"
class="font-mono textarea floating-input"
autofocus
rows="8"
placeholder=" "
/>
<label for="import-curl">
{{ $t("request.enter_curl") }}
</label>
<div ref="curlEditor" class="border border-dividerLight rounded"></div>
</div>
</template>
<template #footer>
<span>
<span class="flex">
<ButtonPrimary
:label="$t('import.title')"
:label="$t('import.title').toString()"
@click.native="handleImport"
/>
<ButtonSecondary
:label="$t('action.cancel')"
:label="$t('action.cancel').toString()"
@click.native="hideModal"
/>
</span>
@@ -30,108 +24,114 @@
</SmartModal>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { ref, useContext } from "@nuxtjs/composition-api"
import parseCurlCommand from "~/helpers/curlparser"
import { useCodemirror } from "~/helpers/editor/codemirror"
import {
HoppRESTHeader,
HoppRESTParam,
makeRESTRequest,
} from "~/helpers/types/HoppRESTRequest"
import { setRESTRequest } from "~/newstore/RESTSession"
import "codemirror/mode/shell/shell"
export default defineComponent({
props: {
show: Boolean,
},
emits: ["hide-modal"],
data() {
return {
curl: "",
}
},
methods: {
hideModal() {
this.$emit("hide-modal")
},
handleImport() {
const text = this.curl
try {
const parsedCurl = parseCurlCommand(text)
const { origin, pathname } = new URL(
parsedCurl.url.replace(/"/g, "").replace(/'/g, "")
)
const endpoint = origin + pathname
const headers: HoppRESTHeader[] = []
const params: HoppRESTParam[] = []
if (parsedCurl.query) {
for (const key of Object.keys(parsedCurl.query)) {
const val = parsedCurl.query[key]!
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
if (Array.isArray(val)) {
val.forEach((value) => {
params.push({
key,
value,
active: true,
})
})
} else {
params.push({
key,
value: val!,
active: true,
})
}
}
}
if (parsedCurl.headers) {
for (const key of Object.keys(parsedCurl.headers)) {
headers.push({
const curl = ref("")
const curlEditor = ref<any | null>(null)
useCodemirror(curlEditor, curl, {
extendedEditorConfig: {
mode: "application/x-sh",
placeholder: t("request.enter_curl").toString(),
},
linter: null,
completer: null,
})
defineProps<{ show: boolean }>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const hideModal = () => {
emit("hide-modal")
}
const handleImport = () => {
const text = curl.value
try {
const parsedCurl = parseCurlCommand(text)
const { origin, pathname } = new URL(
parsedCurl.url.replace(/"/g, "").replace(/'/g, "")
)
const endpoint = origin + pathname
const headers: HoppRESTHeader[] = []
const params: HoppRESTParam[] = []
if (parsedCurl.query) {
for (const key of Object.keys(parsedCurl.query)) {
const val = parsedCurl.query[key]!
if (Array.isArray(val)) {
val.forEach((value) => {
params.push({
key,
value: parsedCurl.headers[key],
value,
active: true,
})
}
}
const method = parsedCurl.method.toUpperCase()
// let rawInput = false
// let rawParams: any | null = null
// if (parsedCurl.data) {
// rawInput = true
// rawParams = parsedCurl.data
// }
this.showCurlImportModal = false
setRESTRequest(
makeRESTRequest({
name: "Untitled request",
endpoint,
method,
params,
headers,
preRequestScript: "",
testScript: "",
auth: {
authType: "none",
authActive: true,
},
body: {
contentType: "application/json",
body: "",
},
})
)
} catch (e) {
console.error(e)
this.$toast.error(this.$t("error.curl_invalid_format").toString(), {
icon: "error_outline",
} else {
params.push({
key,
value: val!,
active: true,
})
}
}
}
if (parsedCurl.headers) {
for (const key of Object.keys(parsedCurl.headers)) {
headers.push({
key,
value: parsedCurl.headers[key],
active: true,
})
}
this.hideModal()
},
},
})
}
const method = parsedCurl.method.toUpperCase()
setRESTRequest(
makeRESTRequest({
name: "Untitled request",
endpoint,
method,
params,
headers,
preRequestScript: "",
testScript: "",
auth: {
authType: "none",
authActive: true,
},
body: {
contentType: "application/json",
body: "",
},
})
)
} catch (e) {
console.error(e)
$toast.error(t("error.curl_invalid_format").toString(), {
icon: "error_outline",
})
}
hideModal()
}
</script>

View File

@@ -47,27 +47,7 @@
/>
</div>
</div>
<div v-if="bulkMode" class="flex">
<textarea-autosize
v-model="bulkParams"
v-focus
name="bulk-parameters"
class="
bg-transparent
border-b border-dividerLight
flex
font-mono font-medium
flex-1
py-2
px-4
whitespace-pre
resize-y
overflow-auto
"
rows="10"
:placeholder="$t('state.bulk_mode_placeholder')"
/>
</div>
<div v-if="bulkMode" ref="bulkEditor"></div>
<div v-else>
<div
v-for="(param, index) in params$"
@@ -96,7 +76,7 @@
<input
v-else
class="bg-transparent flex flex-1 py-2 px-4"
:placeholder="$t('count.parameter', { count: index + 1 })"
:placeholder="$t('count.parameter', { count: index + 1 }).toString()"
:name="'param' + index"
:value="param.key"
autofocus
@@ -130,7 +110,7 @@
<input
v-else
class="bg-transparent flex flex-1 py-2 px-4"
:placeholder="$t('count.value', { count: index + 1 })"
:placeholder="$t('count.value', { count: index + 1 }).toString()"
:name="'value' + index"
:value="param.value"
@change="
@@ -202,13 +182,9 @@
</AppSection>
</template>
<script lang="ts">
import {
defineComponent,
ref,
useContext,
watch,
} from "@nuxtjs/composition-api"
<script setup lang="ts">
import { ref, useContext, watch } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { HoppRESTParam } from "~/helpers/types/HoppRESTRequest"
import { useReadonlyStream } from "~/helpers/utils/composables"
import {
@@ -220,72 +196,74 @@ import {
setRESTParams,
} from "~/newstore/RESTSession"
import { useSetting } from "~/newstore/settings"
import "codemirror/mode/yaml/yaml"
export default defineComponent({
setup() {
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const bulkMode = ref(false)
const bulkParams = ref("")
const bulkMode = ref(false)
const bulkParams = ref("")
watch(bulkParams, () => {
try {
const transformation = bulkParams.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setRESTParams(transformation)
} catch (e) {
$toast.error(t("error.something_went_wrong").toString(), {
icon: "error_outline",
})
console.error(e)
}
watch(bulkParams, () => {
try {
const transformation = bulkParams.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
value: item.substring(item.indexOf(":") + 1).trim(),
active: !item.trim().startsWith("//"),
}))
setRESTParams(transformation)
} catch (e) {
$toast.error(t("error.something_went_wrong").toString(), {
icon: "error_outline",
})
return {
params$: useReadonlyStream(restParams$, []),
EXPERIMENTAL_URL_BAR_ENABLED: useSetting("EXPERIMENTAL_URL_BAR_ENABLED"),
bulkMode,
bulkParams,
}
},
watch: {
params$: {
handler(newValue) {
if (
(newValue[newValue.length - 1]?.key !== "" ||
newValue[newValue.length - 1]?.value !== "") &&
newValue.length
)
this.addParam()
},
deep: true,
},
},
// mounted() {
// if (!this.params$?.length) {
// this.addParam()
// }
// },
methods: {
addParam() {
addRESTParam({ key: "", value: "", active: true })
},
updateParam(index: number, item: HoppRESTParam) {
updateRESTParam(index, item)
},
deleteParam(index: number) {
deleteRESTParam(index)
},
clearContent() {
deleteAllRESTParams()
},
},
console.error(e)
}
})
const bulkEditor = ref<any | null>(null)
useCodemirror(bulkEditor, bulkParams, {
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: t("state.bulk_mode_placeholder").toString(),
},
linter: null,
completer: null,
})
const params$ = useReadonlyStream(restParams$, [])
watch(
params$,
(newValue) => {
if (
(newValue[newValue.length - 1]?.key !== "" ||
newValue[newValue.length - 1]?.value !== "") &&
newValue.length
)
addParam()
},
{ deep: true }
)
const addParam = () => {
addRESTParam({ key: "", value: "", active: true })
}
const updateParam = (index: number, item: HoppRESTParam) => {
updateRESTParam(index, item)
}
const deleteParam = (index: number) => {
deleteRESTParam(index)
}
const clearContent = () => {
deleteAllRESTParams()
}
const EXPERIMENTAL_URL_BAR_ENABLED = useSetting("EXPERIMENTAL_URL_BAR_ENABLED")
</script>

View File

@@ -24,6 +24,13 @@
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear')"
@@ -34,17 +41,7 @@
</div>
<div class="border-b border-dividerLight flex">
<div class="border-r border-dividerLight w-2/3">
<SmartJsEditor
v-model="preRequestScript"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
complete-mode="pre"
/>
<div ref="preRrequestEditor"></div>
</div>
<div
class="
@@ -84,29 +81,44 @@
</AppSection>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
import { usePreRequestScript } from "~/newstore/RESTSession"
import preRequestScriptSnippets from "~/helpers/preRequestScriptSnippets"
import snippets from "~/helpers/preRequestScriptSnippets"
import "codemirror/mode/javascript/javascript"
import { useCodemirror } from "~/helpers/editor/codemirror"
import linter from "~/helpers/editor/linting/preRequest"
import completer from "~/helpers/editor/completion/preRequest"
export default defineComponent({
setup() {
const preRequestScript = usePreRequestScript()
const {
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const useSnippet = (script: string) => {
preRequestScript.value += script
}
const preRequestScript = usePreRequestScript()
const clearContent = () => {
preRequestScript.value = ""
}
const preRrequestEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)
return {
preRequestScript,
snippets: preRequestScriptSnippets,
useSnippet,
clearContent,
}
},
})
useCodemirror(
preRrequestEditor,
preRequestScript,
reactive({
extendedEditorConfig: {
mode: "application/javascript",
lineWrapping: linewrapEnabled,
placeholder: t("preRequest.javascript_code").toString(),
},
linter,
completer,
})
)
const useSnippet = (script: string) => {
preRequestScript.value += script
}
const clearContent = () => {
preRequestScript.value = ""
}
</script>

View File

@@ -24,6 +24,13 @@
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear')"
@@ -55,82 +62,87 @@
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
v-model="rawParamsBody"
:lang="rawInputEditorLang"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div>
<div ref="rawBodyParameters"></div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { computed, reactive, ref, useContext } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { getEditorLangForMimeType } from "~/helpers/editorutils"
import { pluckRef } from "~/helpers/utils/composables"
import { useRESTRequestBody } from "~/newstore/RESTSession"
import "codemirror/mode/yaml/yaml"
import "codemirror/mode/xml/xml"
import "codemirror/mode/css/css"
import "codemirror/mode/htmlmixed/htmlmixed"
import "codemirror/mode/javascript/javascript"
export default defineComponent({
props: {
contentType: {
type: String,
required: true,
const props = defineProps<{
contentType: string
}>()
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const rawParamsBody = pluckRef(useRESTRequestBody(), "body")
const prettifyIcon = ref("align-left")
const rawInputEditorLang = computed(() =>
getEditorLangForMimeType(props.contentType)
)
const linewrapEnabled = ref(true)
const rawBodyParameters = ref<any | null>(null)
useCodemirror(
rawBodyParameters,
rawParamsBody,
reactive({
extendedEditorConfig: {
lineWrapping: linewrapEnabled,
mode: rawInputEditorLang,
placeholder: t("request.raw_body").toString(),
},
},
setup() {
return {
rawParamsBody: pluckRef(useRESTRequestBody(), "body"),
prettifyIcon: "align-left",
linter: null,
completer: null,
})
)
const clearContent = () => {
rawParamsBody.value = ""
}
const uploadPayload = (e: InputEvent) => {
const file = e.target.files[0]
if (file !== undefined && file !== null) {
const reader = new FileReader()
reader.onload = ({ target }) => {
rawParamsBody.value = target?.result
}
},
computed: {
rawInputEditorLang() {
return getEditorLangForMimeType(this.contentType)
},
},
methods: {
clearContent() {
this.rawParamsBody = ""
},
uploadPayload() {
const file = this.$refs.payload.files[0]
if (file !== undefined && file !== null) {
const reader = new FileReader()
reader.onload = ({ target }) => {
this.rawParamsBody = target.result
}
reader.readAsText(file)
this.$toast.success(this.$t("state.file_imported"), {
icon: "attach_file",
})
} else {
this.$toast.error(this.$t("action.choose_file"), {
icon: "attach_file",
})
}
this.$refs.payload.value = ""
},
prettifyRequestBody() {
try {
const jsonObj = JSON.parse(this.rawParamsBody)
this.rawParamsBody = JSON.stringify(jsonObj, null, 2)
this.prettifyIcon = "check"
setTimeout(() => (this.prettifyIcon = "align-left"), 1000)
} catch (e) {
console.error(e)
this.$toast.error(`${this.$t("error.json_prettify_invalid_body")}`, {
icon: "error_outline",
})
}
},
},
})
reader.readAsText(file)
$toast.success(t("state.file_imported").toString(), {
icon: "attach_file",
})
} else {
$toast.error(t("action.choose_file").toString(), {
icon: "attach_file",
})
}
}
const prettifyRequestBody = () => {
try {
const jsonObj = JSON.parse(rawParamsBody.value)
rawParamsBody.value = JSON.stringify(jsonObj, null, 2)
prettifyIcon.value = "check"
setTimeout(() => (prettifyIcon.value = "align-left"), 1000)
} catch (e) {
console.error(e)
$toast.error(`${t("error.json_prettify_invalid_body")}`, {
icon: "error_outline",
})
}
}
</script>

View File

@@ -10,19 +10,19 @@
"
>
<div class="flex space-x-2 pb-4">
<div class="flex flex-col space-y-4 items-end">
<div class="flex flex-col space-y-4 text-right items-end">
<span class="flex flex-1 items-center">
{{ $t("shortcut.request.send_request") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.show_all") }}
</span>
<!-- <span class="flex flex-1 items-center">
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.command_menu") }}
</span>
<span class="flex flex-1 items-center">
{{ $t("shortcut.general.help_menu") }}
</span> -->
</span>
</div>
<div class="flex flex-col space-y-4">
<div class="flex">
@@ -33,12 +33,12 @@
<span class="shortcut-key">{{ getSpecialKey() }}</span>
<span class="shortcut-key">K</span>
</div>
<!-- <div class="flex">
<div class="flex">
<span class="shortcut-key">/</span>
</div>
<div class="flex">
<span class="shortcut-key">?</span>
</div> -->
</div>
</div>
</div>
<ButtonSecondary
@@ -102,26 +102,23 @@
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
<script setup lang="ts">
import { computed } from "@nuxtjs/composition-api"
import findStatusGroup from "~/helpers/findStatusGroup"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
export default defineComponent({
props: {
response: {
type: Object,
default: () => null,
},
},
computed: {
statusCategory() {
return findStatusGroup(this.response.statusCode)
},
},
methods: {
getSpecialKey: getPlatformSpecialKey,
},
const props = defineProps<{
response: HoppRESTResponse
}>()
const statusCategory = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
return findStatusGroup(props.response.statusCode)
})
</script>

View File

@@ -24,6 +24,13 @@
:title="$t('app.wiki')"
svg="help-circle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear')"
@@ -34,17 +41,7 @@
</div>
<div class="border-b border-dividerLight flex">
<div class="border-r border-dividerLight w-2/3">
<SmartJsEditor
v-model="testScript"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
complete-mode="test"
/>
<div ref="testScriptEditor"></div>
</div>
<div
class="
@@ -85,11 +82,38 @@
</template>
<script setup lang="ts">
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
import { useTestScript } from "~/newstore/RESTSession"
import testSnippets from "~/helpers/testSnippets"
import "codemirror/mode/javascript/javascript"
import { useCodemirror } from "~/helpers/editor/codemirror"
import linter from "~/helpers/editor/linting/testScript"
import completer from "~/helpers/editor/completion/testScript"
const {
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const testScript = useTestScript()
const testScriptEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
testScriptEditor,
testScript,
reactive({
extendedEditorConfig: {
mode: "application/javascript",
lineWrapping: linewrapEnabled,
placeholder: t("test.javascript_code").toString(),
},
linter,
completer,
})
)
const useSnippet = (script: string) => {
testScript.value += script
}

View File

@@ -47,7 +47,7 @@
group-hover:text-secondaryDark
"
>
<span class="rounded select-all truncate">
<span class="rounded-sm select-all truncate">
{{ header.key }}
</span>
</span>
@@ -61,7 +61,7 @@
group-hover:text-secondaryDark
"
>
<span class="rounded select-all truncate">
<span class="rounded-sm select-all truncate">
{{ header.value }}
</span>
</span>

View File

@@ -17,6 +17,14 @@
{{ $t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
@@ -44,110 +52,131 @@
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
:value="responseBodyText"
:lang="'html'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<iframe
ref="previewFrame"
:class="{ hidden: !previewEnabled }"
class="covers-response"
src="about:blank"
></iframe>
</div>
<div v-show="!previewEnabled" ref="htmlResponse"></div>
<iframe
v-show="previewEnabled"
ref="previewFrame"
class="covers-response"
src="about:blank"
></iframe>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
<script setup lang="ts">
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/xml/xml"
import "codemirror/mode/javascript/javascript"
import "codemirror/mode/css/css"
import "codemirror/mode/htmlmixed/htmlmixed"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
export default defineComponent({
mixins: [TextContentRendererMixin],
props: {
response: { type: Object, default: () => {} },
},
data() {
return {
downloadIcon: "download",
copyIcon: "copy",
previewEnabled: false,
}
},
methods: {
downloadResponse() {
const dataToWrite = this.responseBodyText
const file = new Blob([dataToWrite], { type: "text/html" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
this.downloadIcon = "check"
this.$toast.success(this.$t("state.download_started"), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.downloadIcon = "download"
}, 1000)
},
copyResponse() {
copyToClipboard(this.responseBodyText)
this.copyIcon = "check"
this.$toast.success(this.$t("state.copied_to_clipboard"), {
icon: "content_paste",
})
setTimeout(() => (this.copyIcon = "copy"), 1000)
},
togglePreview() {
this.previewEnabled = !this.previewEnabled
if (this.previewEnabled) {
if (
this.$refs.previewFrame.getAttribute("data-previewing-url") ===
this.url
)
return
// Use DOMParser to parse document HTML.
const previewDocument = new DOMParser().parseFromString(
this.responseBodyText,
"text/html"
)
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
previewDocument.head.innerHTML =
`<base href="${this.url}">` + previewDocument.head.innerHTML
// Finally, set the iframe source to the resulting HTML.
this.$refs.previewFrame.srcdoc =
previewDocument.documentElement.outerHTML
this.$refs.previewFrame.setAttribute("data-previewing-url", this.url)
}
},
},
const props = defineProps<{
response: HoppRESTResponse
}>()
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
}
})
const downloadIcon = ref("download")
const copyIcon = ref("copy")
const previewEnabled = ref(false)
const previewFrame = ref<any | null>(null)
const url = ref("")
const htmlResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
htmlResponse,
responseBodyText,
reactive({
extendedEditorConfig: {
mode: "htmlmixed",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
})
)
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: "text/html" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadIcon.value = "download"
}, 1000)
}
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"
$toast.success(t("state.copied_to_clipboard").toString(), {
icon: "content_paste",
})
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
const togglePreview = () => {
previewEnabled.value = !previewEnabled.value
if (previewEnabled.value) {
if (previewFrame.value.getAttribute("data-previewing-url") === url.value)
return
// Use DOMParser to parse document HTML.
const previewDocument = new DOMParser().parseFromString(
responseBodyText.value,
"text/html"
)
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
previewDocument.head.innerHTML =
`<base href="${url.value}">` + previewDocument.head.innerHTML
// Finally, set the iframe source to the resulting HTML.
previewFrame.value.srcdoc = previewDocument.documentElement.outerHTML
previewFrame.value.setAttribute("data-previewing-url", url.value)
}
}
</script>
<style lang="scss" scoped>
.covers-response {
@apply absolute;
@apply inset-0;
@apply bg-white;
@apply min-h-64;
@apply h-full;
@apply w-full;
@apply border;
@apply border-dividerLight;
@apply z-5;
}
</style>

View File

@@ -27,12 +27,10 @@
/>
</div>
</div>
<div class="flex relative">
<img
class="border-b border-dividerLight flex max-w-full flex-1"
:src="imageSource"
/>
</div>
<img
class="border-b border-dividerLight flex max-w-full flex-1"
:src="imageSource"
/>
</div>
</template>

View File

@@ -13,10 +13,18 @@
justify-between
"
>
<label class="font-semibold text-secondaryLight">
{{ $t("response.body") }}
</label>
<label class="font-semibold text-secondaryLight">{{
$t("response.body")
}}</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-if="response.body"
ref="downloadResponse"
@@ -35,89 +43,237 @@
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
:value="jsonBodyText"
:lang="'json'"
:provide-outline="true"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
<div ref="jsonResponse"></div>
<div
v-if="outlinePath"
class="
bg-primaryLight
border-t border-dividerLight
flex flex-nowrap flex-1
px-2
bottom-0
z-10
sticky
overflow-auto
hide-scrollbar
"
>
<div
v-for="(item, index) in outlinePath"
:key="`item-${index}`"
class="flex items-center"
>
<tippy
ref="outlineOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<template #trigger>
<div v-if="item.kind === 'RootObject'" class="outline">{}</div>
<div v-if="item.kind === 'RootArray'" class="outline">[]</div>
<div v-if="item.kind === 'ArrayMember'" class="outline">
{{ item.index.toString() }}
</div>
<div v-if="item.kind === 'ObjectMember'" class="outline">
{{ item.name }}
</div>
</template>
<div
v-if="item.kind === 'ArrayMember' || item.kind === 'ObjectMember'"
>
<div v-if="item.kind === 'ArrayMember'" class="flex flex-col">
<SmartItem
v-for="(arrayMember, astIndex) in item.astParent.values"
:key="`ast-${astIndex}`"
:label="astIndex.toString()"
@click.native="
() => {
jumpCursor(arrayMember)
outlineOptions[index].tippy().hide()
}
"
/>
</div>
<div v-if="item.kind === 'ObjectMember'" class="flex flex-col">
<SmartItem
v-for="(objectMember, astIndex) in item.astParent.members"
:key="`ast-${astIndex}`"
:label="objectMember.key.value"
@click.native="
() => {
jumpCursor(objectMember)
outlineOptions[index].tippy().hide()
}
"
/>
</div>
</div>
<div v-if="item.kind === 'RootObject'" class="flex flex-col">
<SmartItem
label="{}"
@click.native="
() => {
jumpCursor(item.astValue)
outlineOptions[index].tippy().hide()
}
"
/>
</div>
<div v-if="item.kind === 'RootArray'" class="flex flex-col">
<SmartItem
label="[]"
@click.native="
() => {
jumpCursor(item.astValue)
outlineOptions[index].tippy().hide()
}
"
/>
</div>
</tippy>
<i
v-if="index + 1 !== outlinePath.length"
class="text-secondaryLight opacity-50 material-icons"
>chevron_right</i
>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
<script setup lang="ts">
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/javascript/javascript"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
import { getJSONOutlineAtPos } from "~/helpers/newOutline"
import {
convertIndexToLineCh,
convertLineChToIndex,
} from "~/helpers/editor/utils"
export default defineComponent({
mixins: [TextContentRendererMixin],
props: {
response: { type: Object, default: () => {} },
},
data() {
return {
downloadIcon: "download",
copyIcon: "copy",
}
},
computed: {
jsonBodyText() {
try {
return JSON.stringify(JSON.parse(this.responseBodyText), null, 2)
} catch (e) {
// Most probs invalid JSON was returned, so drop prettification (should we warn ?)
return this.responseBodyText
}
},
responseType() {
return (
this.response.headers.find(
(h) => h.key.toLowerCase() === "content-type"
).value || ""
)
.split(";")[0]
.toLowerCase()
},
},
methods: {
downloadResponse() {
const dataToWrite = this.responseBodyText
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
this.downloadIcon = "check"
this.$toast.success(this.$t("state.download_started"), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.downloadIcon = "download"
}, 1000)
},
copyResponse() {
copyToClipboard(this.responseBodyText)
this.copyIcon = "check"
this.$toast.success(this.$t("state.copied_to_clipboard"), {
icon: "content_paste",
})
setTimeout(() => (this.copyIcon = "copy"), 1000)
},
},
const props = defineProps<{
response: HoppRESTResponse
}>()
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
}
})
const downloadIcon = ref("download")
const copyIcon = ref("copy")
const jsonBodyText = computed(() => {
try {
return JSON.stringify(JSON.parse(responseBodyText.value), null, 2)
} catch (e) {
// Most probs invalid JSON was returned, so drop prettification (should we warn ?)
return responseBodyText.value
}
})
const ast = computed(() => {
try {
return jsonParse(jsonBodyText.value)
} catch (_: any) {
return null
}
})
const outlineOptions = ref<any | null>(null)
const jsonResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
const { cursor } = useCodemirror(
jsonResponse,
jsonBodyText,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
})
)
const jumpCursor = (ast: JSONValue | JSONObjectMember) => {
const pos = convertIndexToLineCh(jsonBodyText.value, ast.start)
pos.line--
cursor.value = pos
}
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadIcon.value = "download"
}, 1000)
}
const outlinePath = computed(() => {
if (ast.value) {
return getJSONOutlineAtPos(
ast.value,
convertLineChToIndex(jsonBodyText.value, cursor.value)
)
} else return null
})
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"
$toast.success(t("state.copied_to_clipboard").toString(), {
icon: "content_paste",
})
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
</script>
<style lang="scss" scoped>
.outline {
@apply cursor-pointer;
@apply flex-grow-0 flex-shrink-0;
@apply text-secondaryLight;
@apply inline-flex;
@apply items-center;
@apply px-2;
@apply py-1;
@apply transition;
@apply hover:text-secondary;
}
</style>

View File

@@ -17,6 +17,14 @@
{{ $t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-if="response.body"
ref="downloadResponse"
@@ -35,80 +43,96 @@
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
:value="responseBodyText"
:lang="'plain_text'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div>
<div ref="rawResponse"></div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
<script setup lang="ts">
import { ref, useContext, computed, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
export default defineComponent({
mixins: [TextContentRendererMixin],
props: {
response: { type: Object, default: () => {} },
},
data() {
return {
downloadIcon: "download",
copyIcon: "copy",
}
},
computed: {
responseType() {
return (
this.response.headers.find(
(h) => h.key.toLowerCase() === "content-type"
).value || ""
)
.split(";")[0]
.toLowerCase()
},
},
methods: {
downloadResponse() {
const dataToWrite = this.responseBodyText
const file = new Blob([dataToWrite], { type: this.responseType })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
this.downloadIcon = "check"
this.$toast.success(this.$t("state.download_started"), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.downloadIcon = "download"
}, 1000)
},
copyResponse() {
copyToClipboard(this.responseBodyText)
this.copyIcon = "check"
this.$toast.success(this.$t("state.copied_to_clipboard"), {
icon: "content_paste",
})
setTimeout(() => (this.copyIcon = "copy"), 1000)
},
},
const props = defineProps<{
response: HoppRESTResponse
}>()
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
}
})
const downloadIcon = ref("download")
const copyIcon = ref("copy")
const responseType = computed(() => {
return (
props.response.headers.find((h) => h.key.toLowerCase() === "content-type")
.value || ""
)
.split(";")[0]
.toLowerCase()
})
const rawResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
rawResponse,
responseBodyText,
reactive({
extendedEditorConfig: {
mode: "text/plain",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
})
)
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: responseType.value })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadIcon.value = "download"
}, 1000)
}
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"
$toast.success(t("state.copied_to_clipboard").toString(), {
icon: "content_paste",
})
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
</script>

View File

@@ -17,6 +17,14 @@
{{ $t("response.body") }}
</label>
<div class="flex">
<ButtonSecondary
v-if="response.body"
v-tippy="{ theme: 'tooltip' }"
:title="$t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
svg="corner-down-left"
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-if="response.body"
ref="downloadResponse"
@@ -35,80 +43,97 @@
/>
</div>
</div>
<div class="relative">
<SmartAceEditor
:value="responseBodyText"
:lang="'xml'"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
readOnly: true,
showPrintMargin: false,
useWorker: false,
}"
styles="border-b border-dividerLight"
/>
</div>
<div ref="xmlResponse"></div>
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
<script setup lang="ts">
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
import { useCodemirror } from "~/helpers/editor/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import "codemirror/mode/xml/xml"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
export default defineComponent({
mixins: [TextContentRendererMixin],
props: {
response: { type: Object, default: () => {} },
},
data() {
return {
copyIcon: "copy",
downloadIcon: "download",
}
},
computed: {
responseType() {
return (
this.response.headers.find(
(h) => h.key.toLowerCase() === "content-type"
).value || ""
)
.split(";")[0]
.toLowerCase()
},
},
methods: {
downloadResponse() {
const dataToWrite = this.responseBodyText
const file = new Blob([dataToWrite], { type: this.responseType })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
this.downloadIcon = "check"
this.$toast.success(this.$t("state.download_started"), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
this.downloadIcon = "download"
}, 1000)
},
copyResponse() {
copyToClipboard(this.responseBodyText)
this.copyIcon = "check"
this.$toast.success(this.$t("state.copied_to_clipboard"), {
icon: "content_paste",
})
setTimeout(() => (this.copyIcon = "copy"), 1000)
},
},
const props = defineProps<{
response: HoppRESTResponse
}>()
const {
$toast,
app: { i18n },
} = useContext()
const t = i18n.t.bind(i18n)
const responseBodyText = computed(() => {
if (
props.response.type === "loading" ||
props.response.type === "network_fail"
)
return ""
if (typeof props.response.body === "string") return props.response.body
else {
const res = new TextDecoder("utf-8").decode(props.response.body)
// HACK: Temporary trailing null character issue from the extension fix
return res.replace(/\0+$/, "")
}
})
const downloadIcon = ref("download")
const copyIcon = ref("copy")
const responseType = computed(() => {
return (
props.response.headers.find((h) => h.key.toLowerCase() === "content-type")
.value || ""
)
.split(";")[0]
.toLowerCase()
})
const xmlResponse = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
xmlResponse,
responseBodyText,
reactive({
extendedEditorConfig: {
mode: "application/xml",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
})
)
const downloadResponse = () => {
const dataToWrite = responseBodyText.value
const file = new Blob([dataToWrite], { type: responseType.value })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO get uri from meta
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
document.body.appendChild(a)
a.click()
downloadIcon.value = "check"
$toast.success(t("state.download_started").toString(), {
icon: "downloading",
})
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
downloadIcon.value = "download"
}, 1000)
}
const copyResponse = () => {
copyToClipboard(responseBodyText.value)
copyIcon.value = "check"
$toast.success(t("state.copied_to_clipboard").toString(), {
icon: "content_paste",
})
setTimeout(() => (copyIcon.value = "copy"), 1000)
}
</script>

View File

@@ -17,14 +17,13 @@
{{ title }}
</label>
</div>
<div ref="log" name="log" class="realtime-log">
<div name="log" class="realtime-log">
<span v-if="log" class="space-y-2">
<span
v-for="(entry, index) in log"
:key="`entry-${index}`"
:style="{ color: entry.color }"
>{{ entry.ts }}{{ getSourcePrefix(entry.source)
}}{{ entry.payload }}</span
>{{ entry.ts }}{{ source(entry.source) }}{{ entry.payload }}</span
>
</span>
<span v-else>{{ $t("response.waiting_for_connection") }}</span>
@@ -32,27 +31,14 @@
</div>
</template>
<script>
import { defineComponent } from "@nuxtjs/composition-api"
import { getSourcePrefix } from "~/helpers/utils/string"
<script setup lang="ts">
import { getSourcePrefix as source } from "~/helpers/utils/string"
export default defineComponent({
props: {
log: { type: Array, default: () => [] },
title: {
type: String,
default: "",
},
},
updated() {
this.$nextTick(function () {
if (this.$refs.log) {
this.$refs.log.scrollBy(0, this.$refs.log.scrollHeight + 100)
}
})
},
methods: {
getSourcePrefix,
defineProps({
log: { type: Array, default: () => [] },
title: {
type: String,
default: "",
},
})
</script>

View File

@@ -145,7 +145,7 @@ import { defineComponent } from "@nuxtjs/composition-api"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
import Paho from "paho-mqtt"
import debounce from "~/helpers/utils/debounce"
import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { useSetting } from "~/newstore/settings"
import useWindowSize from "~/helpers/utils/useWindowSize"

View File

@@ -165,7 +165,7 @@ import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
import { io as Client } from "socket.io-client"
import wildcard from "socketio-wildcard"
import debounce from "~/helpers/utils/debounce"
import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { useSetting } from "~/newstore/settings"
import useWindowSize from "~/helpers/utils/useWindowSize"

View File

@@ -89,8 +89,8 @@
import { defineComponent } from "@nuxtjs/composition-api"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import debounce from "~/helpers/utils/debounce"
export default defineComponent({
components: { Splitpanes, Pane },

View File

@@ -205,8 +205,8 @@
import { defineComponent } from "@nuxtjs/composition-api"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import debounce from "~/helpers/utils/debounce"
import useWindowSize from "~/helpers/utils/useWindowSize"
import { useSetting } from "~/newstore/settings"

View File

@@ -1,282 +0,0 @@
<template>
<div class="show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
<div
v-if="provideOutline"
class="
bg-primaryLight
border-t border-divider
flex flex-nowrap flex-1
py-1
px-4
bottom-0
z-10
sticky
overflow-auto
hide-scrollbar
"
>
<div
v-for="(p, index) in currentPath"
:key="`p-${index}`"
class="
cursor-pointer
flex-grow-0 flex-shrink-0
text-secondaryLight
inline-flex
items-center
hover:text-secondary
"
>
<span @click="onBlockClick(index)">
{{ p }}
</span>
<i v-if="index + 1 !== currentPath.length" class="mx-2 material-icons">
chevron_right
</i>
<tippy
v-if="siblingDropDownIndex == index"
ref="options"
interactive
trigger="click"
theme="popover"
arrow
>
<SmartItem
v-for="(sibling, siblingIndex) in currentSibling"
:key="`p-${index}-sibling-${siblingIndex}`"
:label="sibling.key ? sibling.key.value : i"
@click.native="goToSibling(sibling)"
/>
</tippy>
</div>
</div>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import { defineComponent } from "@nuxtjs/composition-api"
import jsonParse from "~/helpers/jsonParse"
import debounce from "~/helpers/utils/debounce"
import outline from "~/helpers/outline"
export default defineComponent({
props: {
provideOutline: {
type: Boolean,
default: false,
required: false,
},
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
lang: {
type: String,
default: "json",
},
lint: {
type: Boolean,
default: true,
required: false,
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
outline: outline(),
currentPath: [],
currentSibling: [],
siblingDropDownIndex: null,
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
if (this.lint) this.provideLinting(value)
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
},
lang(value) {
this.editor.getSession().setMode(`ace/mode/${value}`)
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/${this.lang}`,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick().then(() => {
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
if (this.value) editor.setValue(this.value, 1)
this.editor = editor
this.cacheValue = this.value
if (this.lang === "json" && this.provideOutline)
this.initOutline(this.value)
editor.on("change", () => {
const content = editor.getValue()
this.$emit("input", content)
this.cacheValue = content
if (this.provideOutline) debounce(this.initOutline(content), 500)
if (this.lint) this.provideLinting(content)
})
if (this.lang === "json" && this.provideOutline) {
editor.session.selection.on("changeCursor", () => {
const index = editor.session.doc.positionToIndex(
editor.selection.getCursor(),
0
)
const path = this.outline.genPath(index)
if (path.success) {
this.currentPath = path.res
}
})
}
// Disable linting, if lint prop is false
if (this.lint) this.provideLinting(this.value)
},
destroyed() {
this.editor.destroy()
},
methods: {
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
provideLinting: debounce(function (code) {
if (this.lang === "json") {
try {
jsonParse(code)
this.editor.session.setAnnotations([])
} catch (e) {
const pos = this.editor.session
.getDocument()
.indexToPosition(e.start, 0)
this.editor.session.setAnnotations([
{
row: pos.row,
column: pos.column,
text: e.message,
type: "error",
},
])
}
}
}, 2000),
onBlockClick(index) {
if (this.siblingDropDownIndex === index) {
this.clearSiblingList()
} else {
this.currentSibling = this.outline.getSiblings(index)
if (this.currentSibling.length) this.siblingDropDownIndex = index
}
},
clearSiblingList() {
this.currentSibling = []
this.siblingDropDownIndex = null
},
goToSibling(obj) {
this.clearSiblingList()
if (obj.start) {
const pos = this.editor.session.doc.indexToPosition(obj.start, 0)
if (pos) {
this.editor.session.selection.moveCursorTo(pos.row, pos.column, true)
this.editor.session.selection.clearSelection()
this.editor.scrollToLine(pos.row, false, true, null)
}
}
},
initOutline: debounce(function (content) {
if (this.lang === "json") {
try {
this.outline.init(content)
if (content[0] === "[") this.currentPath.push("[]")
else this.currentPath.push("{}")
} catch (e) {
console.log("Outline error: ", e)
}
}
}),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -150,6 +150,16 @@ export default defineComponent({
handleKeystroke(event) {
switch (event.code) {
case "Enter":
event.preventDefault()
if (this.currentSuggestionIndex > -1)
this.forceSuggestion(
this.suggestions.find(
(_item, index) => index === this.currentSuggestionIndex
)
)
break
case "ArrowUp":
event.preventDefault()
this.currentSuggestionIndex =

View File

@@ -483,7 +483,7 @@ export default defineComponent({
line-height: 1.9;
&::before {
@apply text-secondaryDark;
@apply text-secondary;
@apply opacity-25;
@apply pointer-events-none;
@@ -501,7 +501,6 @@ export default defineComponent({
@apply overflow-y-hidden;
@apply resize-none;
@apply focus:outline-none;
@apply transition;
}
.env-input::-webkit-scrollbar {

View File

@@ -1,292 +0,0 @@
<template>
<div class="show-if-initialized" :class="{ initialized }">
<pre ref="editor" :class="styles"></pre>
</div>
</template>
<script>
import ace from "ace-builds"
import "ace-builds/webpack-resolver"
import "ace-builds/src-noconflict/ext-language_tools"
import "ace-builds/src-noconflict/mode-graphqlschema"
import * as esprima from "esprima"
import { defineComponent } from "@nuxtjs/composition-api"
import debounce from "~/helpers/utils/debounce"
import {
getPreRequestScriptCompletions,
getTestScriptCompletions,
performPreRequestLinting,
performTestLinting,
} from "~/helpers/tern"
export default defineComponent({
props: {
value: {
type: String,
default: "",
},
theme: {
type: String,
required: false,
default: null,
},
options: {
type: Object,
default: () => {},
},
styles: {
type: String,
default: "",
},
completeMode: {
type: String,
required: true,
default: "none",
},
},
data() {
return {
initialized: false,
editor: null,
cacheValue: "",
}
},
computed: {
appFontSize() {
return getComputedStyle(document.documentElement).getPropertyValue(
"--body-font-size"
)
},
},
watch: {
value(value) {
if (value !== this.cacheValue) {
this.editor.session.setValue(value, 1)
this.cacheValue = value
if (this.lint) this.provideLinting(value)
}
},
theme() {
this.initialized = false
this.editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick()
.then(() => {
this.initialized = true
})
.catch(() => {
// nextTick shouldn't really ever throw but still
this.initialized = true
})
})
},
options(value) {
this.editor.setOptions(value)
},
},
mounted() {
// const langTools = ace.require("ace/ext/language_tools")
const editor = ace.edit(this.$refs.editor, {
mode: `ace/mode/javascript`,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
...this.options,
})
// Set the theme and show the editor only after it's been set to prevent FOUC.
editor.setTheme(`ace/theme/${this.defineTheme()}`, () => {
this.$nextTick()
.then(() => {
this.initialized = true
})
.catch(() => {
// nextTIck shouldn't really ever throw but still
this.initialized = true
})
})
editor.setFontSize(this.appFontSize)
const completer = {
getCompletions: (
editor,
_session,
{ row, column },
_prefix,
callback
) => {
if (this.completeMode === "pre") {
getPreRequestScriptCompletions(editor.getValue(), row, column)
.then((res) => {
callback(
null,
res.completions.map((r, index, arr) => ({
name: r.name,
value: r.name,
score: (arr.length - index) / arr.length,
meta: r.type,
}))
)
})
.catch(() => callback(null, []))
} else if (this.completeMode === "test") {
getTestScriptCompletions(editor.getValue(), row, column)
.then((res) => {
callback(
null,
res.completions.map((r, index, arr) => ({
name: r.name,
value: r.name,
score: (arr.length - index) / arr.length,
meta: r.type,
}))
)
})
.catch(() => callback(null, []))
}
},
}
editor.completers = [completer]
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.cacheValue = content
this.provideLinting(content)
})
this.provideLinting(this.value)
},
destroyed() {
this.editor.destroy()
},
methods: {
defineTheme() {
if (this.theme) {
return this.theme
}
const strip = (str) =>
str.replace(/#/g, "").replace(/ /g, "").replace(/"/g, "")
return strip(
window
.getComputedStyle(document.documentElement)
.getPropertyValue("--editor-theme")
)
},
provideLinting: debounce(function (code) {
let results = []
const lintFunc =
this.completeMode === "pre"
? performPreRequestLinting
: performTestLinting
lintFunc(code)
.then((semanticLints) => {
results = results.concat(
semanticLints.map((lint) => ({
row: lint.from.line,
column: lint.from.ch,
text: `[semantic] ${lint.message}`,
type: "error",
}))
)
try {
const res = esprima.parseScript(code, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err) => {
const pos = this.editor.session
.getDocument()
.indexToPosition(err.index, 0)
return {
row: pos.row,
column: pos.column,
text: `[syntax] ${err.description}`,
type: "error",
}
})
)
}
} catch (e) {
const pos = this.editor.session
.getDocument()
.indexToPosition(e.index, 0)
results = results.concat([
{
row: pos.row,
column: pos.column,
text: `[syntax] ${e.description}`,
type: "error",
},
])
}
this.editor.session.setAnnotations(results)
})
.catch(() => {
try {
const res = esprima.parseScript(code, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err) => {
const pos = this.editor.session
.getDocument()
.indexToPosition(err.index, 0)
return {
row: pos.row,
column: pos.column,
text: `[syntax] ${err.description}`,
type: "error",
}
})
)
}
} catch (e) {
const pos = this.editor.session
.getDocument()
.indexToPosition(e.index, 0)
results = results.concat([
{
row: pos.row,
column: pos.column,
text: `[syntax] ${e.description}`,
type: "error",
},
])
}
this.editor.session.setAnnotations(results)
})
}, 2000),
},
})
</script>
<style scoped lang="scss">
.show-if-initialized {
&.initialized {
@apply opacity-100;
}
& > * {
@apply transition-none;
}
}
</style>

View File

@@ -195,7 +195,7 @@ export class GQLConnection {
method: "post",
url,
headers: {
...headers,
...finalHeaders,
"content-type": "application/json",
},
data: JSON.stringify({

View File

@@ -15,16 +15,16 @@ describe("getEditorLangForMimeType", () => {
expect(getEditorLangForMimeType("text/html")).toMatch("html")
})
test("returns 'plain_text' for plain text mime", () => {
expect(getEditorLangForMimeType("text/plain")).toMatch("plain_text")
test("returns 'text/x-yaml' for plain text mime", () => {
expect(getEditorLangForMimeType("text/plain")).toMatch("text/x-yaml")
})
test("returns 'plain_text' for unimplemented mimes", () => {
expect(getEditorLangForMimeType("image/gif")).toMatch("plain_text")
test("returns 'text/x-yaml' for unimplemented mimes", () => {
expect(getEditorLangForMimeType("image/gif")).toMatch("text/x-yaml")
})
test("returns 'plain_text' for null/undefined mimes", () => {
expect(getEditorLangForMimeType(null)).toMatch("plain_text")
expect(getEditorLangForMimeType(undefined)).toMatch("plain_text")
test("returns 'text/x-yaml' for null/undefined mimes", () => {
expect(getEditorLangForMimeType(null)).toMatch("text/x-yaml")
expect(getEditorLangForMimeType(undefined)).toMatch("text/x-yaml")
})
})

View File

@@ -150,8 +150,6 @@ function getCodegenGeneralRESTInfo(
.map((x) => ({ ...x, active: true }))
: request.effectiveFinalHeaders.map((x) => ({ ...x, active: true }))
console.log(finalHeaders)
return {
name: request.name,
uri: request.effectiveFinalURL,

View File

@@ -24,7 +24,7 @@ export const CLibcurlCodegen = {
`curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "${method}");`
)
requestString.push(
`curl_easy_setopt(hnd, CURLOPT_URL, "${url}${pathName}${queryString}");`
`curl_easy_setopt(hnd, CURLOPT_URL, "${url}${pathName}?${queryString}");`
)
requestString.push(`struct curl_slist *headers = NULL;`)

View File

@@ -48,7 +48,7 @@ export const CsRestsharpCodegen = {
// create client and request
requestString.push(`var client = new RestClient("${url}");\n\n`)
requestString.push(
`var request = new RestRequest("${pathName}${queryString}", ${requestDataFormat});\n\n`
`var request = new RestRequest("${pathName}?${queryString}", ${requestDataFormat});\n\n`
)
// authentification

View File

@@ -19,7 +19,7 @@ export const CurlCodegen = {
}) => {
const requestString = []
requestString.push(`curl -X ${method}`)
requestString.push(` '${url}${pathName}${queryString}'`)
requestString.push(` '${url}${pathName}?${queryString}'`)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`
requestString.push(

View File

@@ -25,7 +25,7 @@ export const GoNativeCodegen = {
const requestBody = rawInput ? rawParams : rawRequestBody
if (method === "GET") {
requestString.push(
`req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}")\n`
`req, err := http.NewRequest("${method}", "${url}${pathName}?${queryString}")\n`
)
}
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
@@ -33,11 +33,11 @@ export const GoNativeCodegen = {
if (isJSONContentType(contentType)) {
requestString.push(`var reqBody = []byte(\`${requestBody}\`)\n\n`)
requestString.push(
`req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}", bytes.NewBuffer(reqBody))\n`
`req, err := http.NewRequest("${method}", "${url}${pathName}?${queryString}", bytes.NewBuffer(reqBody))\n`
)
} else if (contentType.includes("x-www-form-urlencoded")) {
requestString.push(
`req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}", strings.NewReader("${requestBody}"))\n`
`req, err := http.NewRequest("${method}", "${url}${pathName}?${queryString}", strings.NewReader("${requestBody}"))\n`
)
}
}

View File

@@ -39,7 +39,7 @@ export const JavaOkhttpCodegen = {
}
requestString.push("Request request = new Request.Builder()")
requestString.push(`.url("${url}${pathName}${queryString}")`)
requestString.push(`.url("${url}${pathName}?${queryString}")`)
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
requestString.push(`.method("${method}", body)`)

View File

@@ -32,7 +32,7 @@ export const JavaUnirestCodegen = {
// create client and request
const verb = verbs.find((v) => v.verb === method)
requestString.push(
`HttpResponse<String> response = Unirest.${verb.unirestMethod}("${url}${pathName}${queryString}")\n`
`HttpResponse<String> response = Unirest.${verb.unirestMethod}("${url}${pathName}?${queryString}")\n`
)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`

View File

@@ -21,7 +21,7 @@ export const JavascriptFetchCodegen = {
}) => {
const requestString = []
let genHeaders = []
requestString.push(`fetch("${url}${pathName}${queryString}", {\n`)
requestString.push(`fetch("${url}${pathName}?${queryString}", {\n`)
requestString.push(` method: "${method}",\n`)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`

View File

@@ -21,7 +21,7 @@ export const JavascriptJqueryCodegen = {
const genHeaders = []
requestString.push(
`jQuery.ajax({\n url: "${url}${pathName}${queryString}"`
`jQuery.ajax({\n url: "${url}${pathName}?${queryString}"`
)
requestString.push(`,\n method: "${method.toUpperCase()}"`)
const requestBody = rawInput ? rawParams : rawRequestBody

View File

@@ -25,7 +25,7 @@ export const JavascriptXhrCodegen = {
const user = auth === "Basic Auth" ? `'${httpUser}'` : null
const password = auth === "Basic Auth" ? `'${httpPassword}'` : null
requestString.push(
`xhr.open('${method}', '${url}${pathName}${queryString}', true, ${user}, ${password})`
`xhr.open('${method}', '${url}${pathName}?${queryString}', true, ${user}, ${password})`
)
if (auth === "Bearer Token" || auth === "OAuth 2.0") {
requestString.push(

View File

@@ -22,7 +22,7 @@ export const NodejsAxiosCodegen = {
const requestBody = rawInput ? rawParams : rawRequestBody
requestString.push(
`axios.${method.toLowerCase()}('${url}${pathName}${queryString}'`
`axios.${method.toLowerCase()}('${url}${pathName}?${queryString}'`
)
if (requestBody.length !== 0) {
requestString.push(", ")

View File

@@ -24,7 +24,7 @@ export const NodejsNativeCodegen = {
requestString.push(`const http = require('http');\n\n`)
requestString.push(`const url = '${url}${pathName}${queryString}';\n`)
requestString.push(`const url = '${url}${pathName}?${queryString}';\n`)
requestString.push(`const options = {\n`)
requestString.push(` method: '${method}',\n`)

View File

@@ -25,7 +25,7 @@ export const NodejsRequestCodegen = {
requestString.push(`const request = require('request');\n`)
requestString.push(`const options = {\n`)
requestString.push(` method: '${method.toLowerCase()}',\n`)
requestString.push(` url: '${url}${pathName}${queryString}'`)
requestString.push(` url: '${url}${pathName}?${queryString}'`)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`

View File

@@ -25,7 +25,7 @@ export const NodejsUnirestCodegen = {
requestString.push(`const unirest = require('unirest');\n`)
requestString.push(`const req = unirest(\n`)
requestString.push(
`'${method.toLowerCase()}', '${url}${pathName}${queryString}')\n`
`'${method.toLowerCase()}', '${url}${pathName}?${queryString}')\n`
)
if (auth === "Basic Auth") {

View File

@@ -25,7 +25,7 @@ export const PhpCurlCodegen = {
requestString.push(`<?php\n`)
requestString.push(`$curl = curl_init();\n`)
requestString.push(`curl_setopt_array($curl, array(\n`)
requestString.push(` CURLOPT_URL => "${url}${pathName}${queryString}",\n`)
requestString.push(` CURLOPT_URL => "${url}${pathName}?${queryString}",\n`)
requestString.push(` CURLOPT_RETURNTRANSFER => true,\n`)
requestString.push(` CURLOPT_ENCODING => "",\n`)
requestString.push(` CURLOPT_MAXREDIRS => 10,\n`)

View File

@@ -26,7 +26,7 @@ export const PowershellRestmethodCodegen = {
let variables = ""
requestString.push(
`Invoke-RestMethod -Method '${formattedMethod}' -Uri '${url}${pathName}${queryString}'`
`Invoke-RestMethod -Method '${formattedMethod}' -Uri '${url}${pathName}?${queryString}'`
)
const requestBody = rawInput ? rawParams : rawRequestBody

View File

@@ -91,7 +91,7 @@ export const PythonHttpClientCodegen = {
}
}
requestString.push(
`conn.request("${method}", "${pathName}${queryString}", payload, headers)\n`
`conn.request("${method}", "${pathName}?${queryString}", payload, headers)\n`
)
requestString.push(`res = conn.getresponse()\n`)
requestString.push(`data = res.read()\n`)

View File

@@ -31,7 +31,7 @@ export const PythonRequestsCodegen = {
const genHeaders = []
requestString.push(`import requests\n\n`)
requestString.push(`url = '${url}${pathName}${queryString}'\n`)
requestString.push(`url = '${url}${pathName}?${queryString}'\n`)
// auth headers
if (auth === "Basic Auth") {
@@ -58,7 +58,7 @@ export const PythonRequestsCodegen = {
requestString.push(...printHeaders(genHeaders))
requestString.push(`response = requests.request(\n`)
requestString.push(` '${method}',\n`)
requestString.push(` '${url}${pathName}${queryString}',\n`)
requestString.push(` '${url}${pathName}?${queryString}',\n`)
}
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
genHeaders.push(`'Content-Type': '${contentType}'`)
@@ -83,7 +83,7 @@ export const PythonRequestsCodegen = {
}
requestString.push(`response = requests.request(\n`)
requestString.push(` '${method}',\n`)
requestString.push(` '${url}${pathName}${queryString}',\n`)
requestString.push(` '${url}${pathName}?${queryString}',\n`)
requestString.push(` data=data,\n`)
}

View File

@@ -35,7 +35,7 @@ export const RubyNetHttpCodeGen = {
// create URI and request
const verb = verbs.find((v) => v.verb === method)
requestString.push(`uri = URI.parse('${url}${pathName}${queryString}')\n`)
requestString.push(`uri = URI.parse('${url}${pathName}?${queryString}')\n`)
requestString.push(`request = Net::HTTP::${verb.rbMethod}.new(uri)`)
// content type

View File

@@ -30,7 +30,7 @@ export const SalesforceApexCodegen = {
requestString.push(`HttpRequest request = new HttpRequest();\n`)
requestString.push(`request.setMethod('${method}');\n`)
requestString.push(
`request.setEndpoint('${url}${pathName}${queryString}');\n\n`
`request.setEndpoint('${url}${pathName}?${queryString}');\n\n`
)
// authentification

View File

@@ -37,7 +37,7 @@ export const ShellHttpieCodegen = {
}
// URL
let escapedUrl = `${url}${pathName}${queryString}`
let escapedUrl = `${url}${pathName}?${queryString}`
escapedUrl = escapedUrl.replace(/'/g, "\\'")
requestString.push(` ${method} $'${escapedUrl}'`)

View File

@@ -19,7 +19,7 @@ export const ShellWgetCodegen = {
}) => {
const requestString = []
requestString.push(`wget -O - --method=${method}`)
requestString.push(` '${url}${pathName}${queryString}'`)
requestString.push(` '${url}${pathName}?${queryString}'`)
if (auth === "Basic Auth") {
const basic = `${httpUser}:${httpPassword}`
requestString.push(

View File

@@ -0,0 +1,215 @@
import CodeMirror from "codemirror"
import "codemirror-theme-github/theme/github.css"
import "codemirror/theme/base16-dark.css"
import "codemirror/theme/tomorrow-night-bright.css"
import "codemirror/lib/codemirror.css"
import "codemirror/addon/lint/lint.css"
import "codemirror/addon/dialog/dialog.css"
import "codemirror/addon/hint/show-hint.css"
import "codemirror/addon/fold/foldgutter.css"
import "codemirror/addon/fold/foldgutter"
import "codemirror/addon/fold/brace-fold"
import "codemirror/addon/fold/comment-fold"
import "codemirror/addon/fold/indent-fold"
import "codemirror/addon/display/autorefresh"
import "codemirror/addon/lint/lint"
import "codemirror/addon/hint/show-hint"
import "codemirror/addon/display/placeholder"
import "codemirror/addon/edit/closebrackets"
import "codemirror/addon/search/search"
import "codemirror/addon/search/searchcursor"
import "codemirror/addon/search/jump-to-line"
import "codemirror/addon/dialog/dialog"
import "codemirror/addon/selection/active-line"
import { watch, onMounted, ref, Ref, useContext } from "@nuxtjs/composition-api"
import { LinterDefinition } from "./linting/linter"
import { Completer } from "./completion"
type CodeMirrorOptions = {
extendedEditorConfig: Omit<CodeMirror.EditorConfiguration, "value">
linter: LinterDefinition | null
completer: Completer | null
}
const DEFAULT_EDITOR_CONFIG: CodeMirror.EditorConfiguration = {
autoRefresh: true,
lineNumbers: true,
foldGutter: true,
autoCloseBrackets: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"Ctrl-Space": "autocomplete",
},
viewportMargin: Infinity,
styleActiveLine: true,
}
/**
* A Vue composable to mount and use Codemirror
*
* NOTE: Make sure to import all the necessary Codemirror modules,
* as this function doesn't import any other than the core
* @param el Reference to the dom node to attach to
* @param value Reference to value to read/write to
* @param options CodeMirror options to pass
*/
export function useCodemirror(
el: Ref<any | null>,
value: Ref<string>,
options: CodeMirrorOptions
): { cm: Ref<CodeMirror.Position | null>; cursor: Ref<CodeMirror.Position> } {
const { $colorMode } = useContext() as any
const cm = ref<CodeMirror.Editor | null>(null)
const cursor = ref<CodeMirror.Position>({ line: 0, ch: 0 })
const updateEditorConfig = () => {
Object.keys(options.extendedEditorConfig).forEach((key) => {
// Only update options which need updating
if (
cm.value &&
cm.value?.getOption(key as any) !==
(options.extendedEditorConfig as any)[key]
) {
cm.value?.setOption(
key as any,
(options.extendedEditorConfig as any)[key]
)
}
})
}
const updateLinterConfig = () => {
if (options.linter) {
cm.value?.setOption("lint", options.linter)
}
}
const updateCompleterConfig = () => {
if (options.completer) {
cm.value?.setOption("hintOptions", {
completeSingle: false,
hint: async (editor: CodeMirror.Editor) => {
const pos = editor.getCursor()
const text = editor.getValue()
const token = editor.getTokenAt(pos)
// It's not a word token, so, just increment to skip to next
if (token.string.toUpperCase() === token.string.toLowerCase())
token.start += 1
const result = await options.completer!(text, pos)
if (!result) return null
return <CodeMirror.Hints>{
from: { line: pos.line, ch: token.start },
to: { line: pos.line, ch: token.end },
list: result.completions
.sort((a, b) => a.score - b.score)
.map((x) => x.text),
}
},
})
}
}
const initialize = () => {
if (!el.value) return
cm.value = CodeMirror(el.value!, DEFAULT_EDITOR_CONFIG)
cm.value.setValue(value.value)
setTheme()
updateEditorConfig()
updateLinterConfig()
updateCompleterConfig()
cm.value.on("change", (instance) => {
// External update propagation (via watchers) should be ignored
if (instance.getValue() !== value.value) {
value.value = instance.getValue()
}
})
cm.value.on("cursorActivity", (instance) => {
cursor.value = instance.getCursor()
})
}
// Boot-up CodeMirror, set the value and listeners
onMounted(() => {
initialize()
})
// Reinitialize if the target ref updates
watch(el, () => {
if (cm.value) {
const parent = cm.value.getWrapperElement()
parent.remove()
cm.value = null
}
initialize()
})
const setTheme = () => {
if (cm.value) {
cm.value?.setOption("theme", getThemeName($colorMode.value))
}
}
const getThemeName = (mode: string) => {
switch (mode) {
case "system":
return "default"
case "light":
return "github"
case "dark":
return "base16-dark"
case "black":
return "tomorrow-night-bright"
default:
return "default"
}
}
// If the editor properties are reactive, watch for updates
watch(() => options.extendedEditorConfig, updateEditorConfig, {
immediate: true,
deep: true,
})
watch(() => options.linter, updateLinterConfig, { immediate: true })
watch(() => options.completer, updateCompleterConfig, { immediate: true })
// Watch value updates
watch(value, (newVal) => {
// Check if we are mounted
if (cm.value) {
// Don't do anything on internal updates
if (cm.value.getValue() !== newVal) {
cm.value.setValue(newVal)
}
}
})
// Push cursor updates
watch(cursor, (value) => {
if (value !== cm.value?.getCursor()) {
cm.value?.focus()
cm.value?.setCursor(value)
}
})
// Watch color mode updates and update theme
watch(() => $colorMode.value, setTheme)
return {
cm,
cursor,
}
}

View File

@@ -0,0 +1,27 @@
import { Ref } from "@nuxtjs/composition-api"
import { GraphQLSchema } from "graphql"
import { getAutocompleteSuggestions } from "graphql-language-service-interface"
import { Completer, CompleterResult, CompletionEntry } from "."
const completer: (schemaRef: Ref<GraphQLSchema | null>) => Completer =
(schemaRef: Ref<GraphQLSchema | null>) => (text, completePos) => {
if (!schemaRef.value) return Promise.resolve(null)
const completions = getAutocompleteSuggestions(schemaRef.value, text, {
line: completePos.line,
character: completePos.ch,
} as any)
return Promise.resolve(<CompleterResult>{
completions: completions.map(
(x, i) =>
<CompletionEntry>{
text: x.label!,
meta: x.detail!,
score: completions.length - i,
}
),
})
}
export default completer

View File

@@ -0,0 +1,23 @@
export type CompletionEntry = {
text: string
meta: string
score: number
}
export type CompleterResult = {
/**
* List of completions to display
*/
completions: CompletionEntry[]
}
export type Completer = (
/**
* The contents of the editor
*/
text: string,
/**
* Position where the completer is fired
*/
completePos: { line: number; ch: number }
) => Promise<CompleterResult | null>

View File

@@ -0,0 +1,24 @@
import { Completer, CompletionEntry } from "."
import { getPreRequestScriptCompletions } from "~/helpers/tern"
const completer: Completer = async (text, completePos) => {
const results = await getPreRequestScriptCompletions(
text,
completePos.line,
completePos.ch
)
const completions = results.completions.map((completion: any, i: number) => {
return <CompletionEntry>{
text: completion.name,
meta: completion.isKeyword ? "keyword" : completion.type,
score: results.completions.length - i,
}
})
return {
completions,
}
}
export default completer

View File

@@ -0,0 +1,24 @@
import { Completer, CompletionEntry } from "."
import { getTestScriptCompletions } from "~/helpers/tern"
export const completer: Completer = async (text, completePos) => {
const results = await getTestScriptCompletions(
text,
completePos.line,
completePos.ch
)
const completions = results.completions.map((completion: any, i: number) => {
return <CompletionEntry>{
text: completion.name,
meta: completion.isKeyword ? "keyword" : completion.type,
score: results.completions.length - i,
}
})
return {
completions,
}
}
export default completer

View File

@@ -0,0 +1,58 @@
import { Ref } from "@nuxtjs/composition-api"
import {
GraphQLError,
GraphQLSchema,
parse as gqlParse,
validate as gqlValidate,
} from "graphql"
import { LinterDefinition, LinterResult } from "./linter"
/**
* Creates a Linter function that can lint a GQL query against a given
* schema
*/
export const createGQLQueryLinter: (
schema: Ref<GraphQLSchema | null>
) => LinterDefinition = (schema: Ref<GraphQLSchema | null>) => (text) => {
if (text === "") return Promise.resolve([])
if (!schema.value) return Promise.resolve([])
try {
const doc = gqlParse(text)
const results = gqlValidate(schema.value, doc).map(
({ locations, message }) =>
<LinterResult>{
from: {
line: locations![0].line - 1,
ch: locations![0].column - 1,
},
to: {
line: locations![0].line - 1,
ch: locations![0].column,
},
message,
severity: "error",
}
)
return Promise.resolve(results)
} catch (e) {
const err = e as GraphQLError
return Promise.resolve([
<LinterResult>{
from: {
line: err.locations![0].line - 1,
ch: err.locations![0].column - 1,
},
to: {
line: err.locations![0].line - 1,
ch: err.locations![0].column,
},
message: err.message,
severity: "error",
},
])
}
}

View File

@@ -0,0 +1,21 @@
import { convertIndexToLineCh } from "../utils"
import { LinterDefinition, LinterResult } from "./linter"
import jsonParse from "~/helpers/jsonParse"
const linter: LinterDefinition = (text) => {
try {
jsonParse(text)
return Promise.resolve([])
} catch (e: any) {
return Promise.resolve([
<LinterResult>{
from: convertIndexToLineCh(text, e.start),
to: convertIndexToLineCh(text, e.end),
message: e.message,
severity: "error",
},
])
}
}
export default linter

View File

@@ -0,0 +1,7 @@
export type LinterResult = {
message: string
severity: "warning" | "error"
from: { line: number; ch: number }
to: { line: number; ch: number }
}
export type LinterDefinition = (text: string) => Promise<LinterResult[]>

View File

@@ -0,0 +1,69 @@
import * as esprima from "esprima"
import { LinterDefinition, LinterResult } from "./linter"
import { performPreRequestLinting } from "~/helpers/tern"
const linter: LinterDefinition = async (text) => {
let results: LinterResult[] = []
// Semantic linting
const semanticLints = await performPreRequestLinting(text)
results = results.concat(
semanticLints.map((lint: any) => ({
from: lint.from,
to: lint.to,
severity: "error",
message: `[semantic] ${lint.message}`,
}))
)
// Syntax linting
try {
const res: any = esprima.parseScript(text, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err: any) => {
const fromPos: { line: number; ch: number } = {
line: err.lineNumber - 1,
ch: err.column - 1,
}
const toPos: { line: number; ch: number } = {
line: err.lineNumber - 1,
ch: err.column,
}
return <LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${err.description}`,
severity: "error",
}
})
)
}
} catch (e) {
const fromPos: { line: number; ch: number } = {
line: e.lineNumber - 1,
ch: e.column - 1,
}
const toPos: { line: number; ch: number } = {
line: e.lineNumber - 1,
ch: e.column,
}
results = results.concat([
<LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${e.description}`,
severity: "error",
},
])
}
return results
}
export default linter

View File

@@ -0,0 +1,69 @@
import * as esprima from "esprima"
import { LinterDefinition, LinterResult } from "./linter"
import { performTestLinting } from "~/helpers/tern"
const linter: LinterDefinition = async (text) => {
let results: LinterResult[] = []
// Semantic linting
const semanticLints = await performTestLinting(text)
results = results.concat(
semanticLints.map((lint: any) => ({
from: lint.from,
to: lint.to,
severity: "error",
message: `[semantic] ${lint.message}`,
}))
)
// Syntax linting
try {
const res: any = esprima.parseScript(text, { tolerant: true })
if (res.errors && res.errors.length > 0) {
results = results.concat(
res.errors.map((err: any) => {
const fromPos: { line: number; ch: number } = {
line: err.lineNumber - 1,
ch: err.column - 1,
}
const toPos: { line: number; ch: number } = {
line: err.lineNumber - 1,
ch: err.column,
}
return <LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${err.description}`,
severity: "error",
}
})
)
}
} catch (e) {
const fromPos: { line: number; ch: number } = {
line: e.lineNumber - 1,
ch: e.column - 1,
}
const toPos: { line: number; ch: number } = {
line: e.lineNumber - 1,
ch: e.column,
}
results = results.concat([
<LinterResult>{
from: fromPos,
to: toPos,
message: `[syntax] ${e.description}`,
severity: "error",
},
])
}
return results
}
export default linter

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2021 GraphQL Contributors
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
import CodeMirror from "codemirror"
import {
LexRules,
ParseRules,
isIgnored,
onlineParser,
State,
} from "graphql-language-service-parser"
/**
* The GraphQL mode is defined as a tokenizer along with a list of rules, each
* of which is either a function or an array.
*
* * Function: Provided a token and the stream, returns an expected next step.
* * Array: A list of steps to take in order.
*
* A step is either another rule, or a terminal description of a token. If it
* is a rule, that rule is pushed onto the stack and the parsing continues from
* that point.
*
* If it is a terminal description, the token is checked against it using a
* `match` function. If the match is successful, the token is colored and the
* rule is stepped forward. If the match is unsuccessful, the remainder of the
* rule is skipped and the previous rule is advanced.
*
* This parsing algorithm allows for incremental online parsing within various
* levels of the syntax tree and results in a structured `state` linked-list
* which contains the relevant information to produce valuable typeaheads.
*/
CodeMirror.defineMode("graphql", (config) => {
const parser = onlineParser({
eatWhitespace: (stream) => stream.eatWhile(isIgnored),
lexRules: LexRules,
parseRules: ParseRules,
editorConfig: { tabSize: 2 },
})
return {
config,
startState: parser.startState,
token: parser.token as unknown as CodeMirror.Mode<any>["token"], // TODO: Check if the types are indeed compatible
indent,
electricInput: /^\s*[})\]]/,
fold: "brace",
lineComment: "#",
closeBrackets: {
pairs: '()[]{}""',
explode: "()[]{}",
},
}
})
// Seems the electricInput type in @types/codemirror is wrong (i.e it is written as electricinput instead of electricInput)
function indent(
this: CodeMirror.Mode<any> & {
electricInput?: RegExp
config?: CodeMirror.EditorConfiguration
},
state: State,
textAfter: string
) {
const levels = state.levels
// If there is no stack of levels, use the current level.
// Otherwise, use the top level, pre-emptively dedenting for close braces.
const level =
!levels || levels.length === 0
? state.indentLevel
: levels[levels.length - 1] -
(this.electricInput?.test(textAfter) ? 1 : 0)
return (level || 0) * (this.config?.indentUnit || 0)
}

View File

@@ -0,0 +1,38 @@
export function convertIndexToLineCh(
text: string,
i: number
): { line: number; ch: number } {
const lines = text.split("\n")
let line = 0
let counter = 0
while (line < lines.length) {
if (i > lines[line].length + counter) {
counter += lines[line].length + 1
line++
} else {
return {
line: line + 1,
ch: i - counter + 1,
}
}
}
throw new Error("Invalid input")
}
export function convertLineChToIndex(
text: string,
lineCh: { line: number; ch: number }
): number {
const textSplit = text.split("\n")
if (textSplit.length < lineCh.line) throw new Error("Invalid position")
const tillLineIndex = textSplit
.slice(0, lineCh.line)
.reduce((acc, line) => acc + line.length + 1, 0)
return tillLineIndex + lineCh.ch
}

View File

@@ -1,12 +1,12 @@
const mimeToMode = {
"text/plain": "plain_text",
"text/html": "html",
"application/xml": "xml",
"application/hal+json": "json",
"application/vnd.api+json": "json",
"application/json": "json",
"text/plain": "text/x-yaml",
"text/html": "htmlmixed",
"application/xml": "application/xml",
"application/hal+json": "application/ld+json",
"application/vnd.api+json": "application/ld+json",
"application/json": "application/ld+json",
}
export function getEditorLangForMimeType(mimeType) {
return mimeToMode[mimeType] || "plain_text"
return mimeToMode[mimeType] || "text/x-yaml"
}

View File

@@ -19,7 +19,75 @@
* - end: int - the end exclusive offset of the syntax error
*
*/
export default function jsonParse(str) {
type JSONEOFValue = {
kind: "EOF"
start: number
end: number
}
type JSONNullValue = {
kind: "Null"
start: number
end: number
}
type JSONNumberValue = {
kind: "Number"
start: number
end: number
value: number
}
type JSONStringValue = {
kind: "String"
start: number
end: number
value: string
}
type JSONBooleanValue = {
kind: "Boolean"
start: number
end: number
value: boolean
}
type JSONPrimitiveValue =
| JSONNullValue
| JSONEOFValue
| JSONStringValue
| JSONNumberValue
| JSONBooleanValue
export type JSONObjectValue = {
kind: "Object"
start: number
end: number
// eslint-disable-next-line no-use-before-define
members: JSONObjectMember[]
}
export type JSONArrayValue = {
kind: "Array"
start: number
end: number
// eslint-disable-next-line no-use-before-define
values: JSONValue[]
}
export type JSONValue = JSONObjectValue | JSONArrayValue | JSONPrimitiveValue
export type JSONObjectMember = {
kind: "Member"
start: number
end: number
key: JSONStringValue
value: JSONValue
}
export default function jsonParse(
str: string
): JSONObjectValue | JSONArrayValue {
string = str
strLen = str.length
start = end = lastEnd = -1
@@ -37,15 +105,15 @@ export default function jsonParse(str) {
}
}
let string
let strLen
let start
let end
let lastEnd
let code
let kind
let string: string
let strLen: number
let start: number
let end: number
let lastEnd: number
let code: number
let kind: string
function parseObj() {
function parseObj(): JSONObjectValue {
const nodeStart = start
const members = []
expect("{")
@@ -63,9 +131,9 @@ function parseObj() {
}
}
function parseMember() {
function parseMember(): JSONObjectMember {
const nodeStart = start
const key = kind === "String" ? curToken() : null
const key = kind === "String" ? (curToken() as JSONStringValue) : null
expect("String")
expect(":")
const value = parseVal()
@@ -73,14 +141,14 @@ function parseMember() {
kind: "Member",
start: nodeStart,
end: lastEnd,
key,
key: key!,
value,
}
}
function parseArr() {
function parseArr(): JSONArrayValue {
const nodeStart = start
const values = []
const values: JSONValue[] = []
expect("[")
if (!skip("]")) {
do {
@@ -96,7 +164,7 @@ function parseArr() {
}
}
function parseVal() {
function parseVal(): JSONValue {
switch (kind) {
case "[":
return parseArr()
@@ -111,14 +179,19 @@ function parseVal() {
lex()
return token
}
return expect("Value")
return expect("Value") as never
}
function curToken() {
return { kind, start, end, value: JSON.parse(string.slice(start, end)) }
function curToken(): JSONPrimitiveValue {
return {
kind: kind as any,
start,
end,
value: JSON.parse(string.slice(start, end)),
}
}
function expect(str) {
function expect(str: string) {
if (kind === str) {
lex()
return
@@ -137,11 +210,17 @@ function expect(str) {
throw syntaxError(`Expected ${str} but found ${found}.`)
}
function syntaxError(message) {
type SyntaxError = {
message: string
start: number
end: number
}
function syntaxError(message: string): SyntaxError {
return { message, start, end }
}
function skip(k) {
function skip(k: string) {
if (kind === k) {
lex()
return true
@@ -227,7 +306,7 @@ function lex() {
function readString() {
ch()
while (code !== 34 && code > 31) {
if (code === 92) {
if (code === (92 as any)) {
// \
ch()
switch (code) {
@@ -299,7 +378,7 @@ function readNumber() {
if (code === 69 || code === 101) {
// E e
ch()
if (code === 43 || code === 45) {
if (code === (43 as any) || code === (45 as any)) {
// + -
ch()
}

View File

@@ -1,10 +1,12 @@
const htmlLens = {
import { Lens } from "./lenses"
const htmlLens: Lens = {
lensName: "response.html",
isSupportedContentType: (contentType) =>
/\btext\/html|application\/xhtml\+xml\b/i.test(contentType),
renderer: "htmlres",
rendererImport: () =>
import("~/components/lenses/renderers/HTMLLensRenderer"),
import("~/components/lenses/renderers/HTMLLensRenderer.vue"),
}
export default htmlLens

View File

@@ -1,4 +1,6 @@
const imageLens = {
import { Lens } from "./lenses"
const imageLens: Lens = {
lensName: "response.image",
isSupportedContentType: (contentType) =>
/\bimage\/(?:gif|jpeg|png|bmp|svg\+xml|x-icon|vnd\.microsoft\.icon)\b/i.test(
@@ -6,7 +8,7 @@ const imageLens = {
),
renderer: "imageres",
rendererImport: () =>
import("~/components/lenses/renderers/ImageLensRenderer"),
import("~/components/lenses/renderers/ImageLensRenderer.vue"),
}
export default imageLens

View File

@@ -1,11 +1,12 @@
import { isJSONContentType } from "../utils/contenttypes"
import { Lens } from "./lenses"
const jsonLens = {
const jsonLens: Lens = {
lensName: "response.json",
isSupportedContentType: isJSONContentType,
renderer: "json",
rendererImport: () =>
import("~/components/lenses/renderers/JSONLensRenderer"),
import("~/components/lenses/renderers/JSONLensRenderer.vue"),
}
export default jsonLens

View File

@@ -1,28 +0,0 @@
import jsonLens from "./jsonLens"
import rawLens from "./rawLens"
import imageLens from "./imageLens"
import htmlLens from "./htmlLens"
import xmlLens from "./xmlLens"
export const lenses = [jsonLens, imageLens, htmlLens, xmlLens, rawLens]
export function getSuitableLenses(response) {
const contentType = response.headers.find((h) => h.key === "content-type")
if (!contentType) return [rawLens]
const result = []
for (const lens of lenses) {
if (lens.isSupportedContentType(contentType.value)) result.push(lens)
}
return result
}
export function getLensRenderers() {
const response = {}
for (const lens of lenses) {
response[lens.renderer] = lens.rendererImport
}
return response
}

View File

@@ -0,0 +1,42 @@
import { HoppRESTResponse } from "../types/HoppRESTResponse"
import jsonLens from "./jsonLens"
import rawLens from "./rawLens"
import imageLens from "./imageLens"
import htmlLens from "./htmlLens"
import xmlLens from "./xmlLens"
export type Lens = {
lensName: string
isSupportedContentType: (contentType: string) => boolean
renderer: string
rendererImport: () => Promise<typeof import("*.vue")>
}
export const lenses: Lens[] = [jsonLens, imageLens, htmlLens, xmlLens, rawLens]
export function getSuitableLenses(response: HoppRESTResponse): Lens[] {
// return empty array if response is loading or error
if (response.type === "loading" || response.type === "network_fail") return []
const contentType = response.headers.find((h) => h.key === "content-type")
if (!contentType) return [rawLens]
const result = []
for (const lens of lenses) {
if (lens.isSupportedContentType(contentType.value)) result.push(lens)
}
return result
}
type LensRenderers = {
[key: string]: Lens["rendererImport"]
}
export function getLensRenderers(): LensRenderers {
const response: LensRenderers = {}
for (const lens of lenses) {
response[lens.renderer] = lens.rendererImport
}
return response
}

View File

@@ -1,8 +0,0 @@
const rawLens = {
lensName: "response.raw",
isSupportedContentType: () => true,
renderer: "raw",
rendererImport: () => import("~/components/lenses/renderers/RawLensRenderer"),
}
export default rawLens

View File

@@ -0,0 +1,11 @@
import { Lens } from "./lenses"
const rawLens: Lens = {
lensName: "response.raw",
isSupportedContentType: () => true,
renderer: "raw",
rendererImport: () =>
import("~/components/lenses/renderers/RawLensRenderer.vue"),
}
export default rawLens

View File

@@ -1,8 +1,11 @@
const xmlLens = {
import { Lens } from "./lenses"
const xmlLens: Lens = {
lensName: "response.xml",
isSupportedContentType: (contentType) => /\bxml\b/i.test(contentType),
renderer: "xmlres",
rendererImport: () => import("~/components/lenses/renderers/XMLLensRenderer"),
rendererImport: () =>
import("~/components/lenses/renderers/XMLLensRenderer.vue"),
}
export default xmlLens

View File

@@ -0,0 +1,100 @@
import {
JSONArrayValue,
JSONObjectMember,
JSONObjectValue,
JSONValue,
} from "./jsonParse"
type RootEntry =
| {
kind: "RootObject"
astValue: JSONObjectValue
}
| {
kind: "RootArray"
astValue: JSONArrayValue
}
type ObjectMemberEntry = {
kind: "ObjectMember"
name: string
astValue: JSONObjectMember
astParent: JSONObjectValue
}
type ArrayMemberEntry = {
kind: "ArrayMember"
index: number
astValue: JSONValue
astParent: JSONArrayValue
}
type PathEntry = RootEntry | ObjectMemberEntry | ArrayMemberEntry
export function getJSONOutlineAtPos(
jsonRootAst: JSONObjectValue | JSONArrayValue,
posIndex: number
): PathEntry[] | null {
try {
const rootObj = jsonRootAst
if (posIndex > rootObj.end || posIndex < rootObj.start)
throw new Error("Invalid position")
let current: JSONValue = rootObj
const path: PathEntry[] = []
if (rootObj.kind === "Object") {
path.push({
kind: "RootObject",
astValue: rootObj,
})
} else {
path.push({
kind: "RootArray",
astValue: rootObj,
})
}
while (current.kind === "Object" || current.kind === "Array") {
if (current.kind === "Object") {
const next: JSONObjectMember | undefined = current.members.find(
(member) => member.start <= posIndex && member.end >= posIndex
)
if (!next) throw new Error("Couldn't find child")
path.push({
kind: "ObjectMember",
name: next.key.value,
astValue: next,
astParent: current,
})
current = next.value
} else {
const nextIndex = current.values.findIndex(
(value) => value.start <= posIndex && value.end >= posIndex
)
if (nextIndex < 0) throw new Error("Couldn't find child")
const next: JSONValue = current.values[nextIndex]
path.push({
kind: "ArrayMember",
index: nextIndex,
astValue: next,
astParent: current,
})
current = next
}
}
return path
} catch (e: any) {
return null
}
}

View File

@@ -1,124 +0,0 @@
import jsonParse from "./jsonParse"
export default () => {
let jsonAST = {}
let path = []
const init = (jsonStr) => {
jsonAST = jsonParse(jsonStr)
linkParents(jsonAST)
}
const setNewText = (jsonStr) => {
init(jsonStr)
path = []
}
const linkParents = (node) => {
if (node.kind === "Object") {
if (node.members) {
node.members.forEach((m) => {
m.parent = node
linkParents(m)
})
}
} else if (node.kind === "Array") {
if (node.values) {
node.values.forEach((v) => {
v.parent = node
linkParents(v)
})
}
} else if (node.kind === "Member") {
if (node.value) {
node.value.parent = node
linkParents(node.value)
}
}
}
const genPath = (index) => {
let output = {}
path = []
let current = jsonAST
if (current.kind === "Object") {
path.push({ label: "{}", obj: "root" })
} else if (current.kind === "Array") {
path.push({ label: "[]", obj: "root" })
}
let over = false
try {
while (!over) {
if (current.kind === "Object") {
let i = 0
let found = false
while (i < current.members.length) {
const m = current.members[i]
if (m.start <= index && m.end >= index) {
path.push({ label: m.key.value, obj: m })
current = current.members[i]
found = true
break
}
i++
}
if (!found) over = true
} else if (current.kind === "Array") {
if (current.values) {
let i = 0
let found = false
while (i < current.values.length) {
const m = current.values[i]
if (m.start <= index && m.end >= index) {
path.push({ label: `[${i.toString()}]`, obj: m })
current = current.values[i]
found = true
break
}
i++
}
if (!found) over = true
} else over = true
} else if (current.kind === "Member") {
if (current.value) {
if (current.value.start <= index && current.value.end >= index) {
current = current.value
} else over = true
} else over = true
} else if (
current.kind === "String" ||
current.kind === "Number" ||
current.kind === "Boolean" ||
current.kind === "Null"
) {
if (current.start <= index && current.end >= index) {
path.push({ label: `${current.value}`, obj: current })
}
over = true
}
}
output = { success: true, res: path.map((p) => p.label) }
} catch (e) {
output = { success: false, res: e }
}
return output
}
const getSiblings = (index) => {
const parent = path[index]?.obj?.parent
if (!parent) return []
else if (parent.kind === "Object") {
return parent.members
} else if (parent.kind === "Array") {
return parent.values
} else return []
}
return {
init,
genPath,
getSiblings,
setNewText,
}
}

View File

@@ -28,4 +28,12 @@ export type HoppRequestSaveContext =
* ID of the request in the team
*/
requestID: string
/**
* ID of the team
*/
teamID?: string
/**
* ID of the collection loaded
*/
collectionID?: string
}

View File

@@ -8,9 +8,9 @@ import { map } from "rxjs/operators"
*
* @returns The constructed object observable
*/
export function constructFromStreams<T>(
streamObj: { [key in keyof T]: Observable<T[key]> }
): Observable<T> {
export function constructFromStreams<T>(streamObj: {
[key in keyof T]: Observable<T[key]>
}): Observable<T> {
return combineLatest(Object.values<Observable<T[keyof T]>>(streamObj)).pipe(
map((streams) => {
const keys = Object.keys(streamObj) as (keyof T)[]

View File

@@ -1,4 +1,4 @@
export const decodeB64StringToArrayBuffer = (input) => {
export function decodeB64StringToArrayBuffer(input: string): ArrayBuffer {
const bytes = Math.floor((input.length / 4) * 3)
const ab = new ArrayBuffer(bytes)
const uarray = new Uint8Array(ab)

View File

@@ -1,12 +0,0 @@
export function getSourcePrefix(source) {
const sourceEmojis = {
// Source used for info messages.
info: "\t [INFO]:\t",
// Source used for client to server messages.
client: "\t⬅ [SENT]:\t",
// Source used for server to client messages.
server: "\t➡ [RECEIVED]:\t",
}
if (Object.keys(sourceEmojis).includes(source)) return sourceEmojis[source]
return ""
}

View File

@@ -0,0 +1,12 @@
const sourceEmojis = {
// Source used for info messages.
info: "\t [INFO]:\t",
// Source used for client to server messages.
client: "\t⬅ [SENT]:\t",
// Source used for server to client messages.
server: "\t➡ [RECEIVED]:\t",
}
export function getSourcePrefix(source: keyof typeof sourceEmojis) {
return sourceEmojis[source]
}

View File

@@ -24,7 +24,7 @@
>
<Pane class="flex flex-1 hide-scrollbar !overflow-auto">
<main class="flex flex-1 w-full">
<nuxt class="flex flex-1" />
<nuxt class="flex overflow-y-auto flex-1" />
</main>
</Pane>
</Splitpanes>

View File

@@ -421,6 +421,7 @@
"file_imported": "File imported",
"finished_in": "Finished in {duration}ms",
"history_deleted": "History deleted",
"linewrap": "Wrap lines",
"loading": "Loading...",
"none": "None",
"nothing_found": "Nothing found for",

View File

@@ -0,0 +1,134 @@
import { resolve } from "path"
import { Module } from "@nuxt/types"
import ts from "typescript"
import chokidar from "chokidar"
const { readdir, writeFile } = require("fs").promises
function titleCase(str: string): string {
return str[0].toUpperCase() + str.substring(1)
}
async function* getFilesInDir(dir: string): AsyncIterable<string> {
const dirents = await readdir(dir, { withFileTypes: true })
for (const dirent of dirents) {
const res = resolve(dir, dirent.name)
if (dirent.isDirectory()) {
yield* getFilesInDir(res)
} else {
yield res
}
}
}
async function getAllVueComponentPaths(): Promise<string[]> {
const vueFilePaths: string[] = []
for await (const f of getFilesInDir("./components")) {
if (f.endsWith(".vue")) {
const componentsIndex = f.split("/").indexOf("components")
vueFilePaths.push(`./${f.split("/").slice(componentsIndex).join("/")}`)
}
}
return vueFilePaths
}
function resolveComponentName(filename: string): string {
const index = filename.split("/").indexOf("components")
return filename
.split("/")
.slice(index + 1)
.filter((x) => x !== "index.vue") // Remove index.vue
.map((x) => x.split(".vue")[0]) // Remove extension
.filter((x) => x.toUpperCase() !== x.toLowerCase()) // Remove non-word stuff
.map((x) => titleCase(x)) // titlecase it
.join("")
}
function createTSImports(components: [string, string][]) {
return components.map(([componentName, componentPath]) => {
return ts.factory.createImportDeclaration(
undefined,
undefined,
ts.factory.createImportClause(
false,
ts.factory.createIdentifier(componentName),
undefined
),
ts.factory.createStringLiteral(componentPath)
)
})
}
function createTSProps(components: [string, string][]) {
return components.map(([componentName]) => {
return ts.factory.createPropertySignature(
undefined,
ts.factory.createIdentifier(componentName),
undefined,
ts.factory.createTypeQueryNode(ts.factory.createIdentifier(componentName))
)
})
}
function generateTypeScriptDef(components: [string, string][]) {
const statements = [
...createTSImports(components),
ts.factory.createModuleDeclaration(
undefined,
[ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
ts.factory.createIdentifier("global"),
ts.factory.createModuleBlock([
ts.factory.createInterfaceDeclaration(
undefined,
undefined,
ts.factory.createIdentifier("__VLS_GlobalComponents"),
undefined,
undefined,
[...createTSProps(components)]
),
]),
ts.NodeFlags.ExportContext |
ts.NodeFlags.GlobalAugmentation |
ts.NodeFlags.ContextFlags
),
]
const source = ts.factory.createSourceFile(
statements,
ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
ts.NodeFlags.None
)
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
})
return printer.printFile(source)
}
async function generateShim() {
const results = await getAllVueComponentPaths()
const fileComponentNameCombo: [string, string][] = results.map((x) => [
resolveComponentName(x),
x,
])
const typescriptString = generateTypeScriptDef(fileComponentNameCombo)
await writeFile(resolve("shims-volar.d.ts"), typescriptString)
}
const module: Module<{}> = async function () {
if (!this.nuxt.options.dev) return
await generateShim()
chokidar.watch(resolve("../components/")).on("all", async () => {
await generateShim()
})
}
export default module

View File

@@ -133,6 +133,7 @@ export default {
"@nuxtjs/composition-api/module",
// https://github.com/antfu/unplugin-vue2-script-setup
"unplugin-vue2-script-setup/nuxt",
"~/modules/emit-volar-types.ts",
],
// Modules (https://go.nuxtjs.dev/config-modules)
@@ -280,7 +281,7 @@ export default {
config.module.rules.push({
test: /\.js$/,
include: /(node_modules)/,
exclude: /(node_modules)\/(ace-builds)|(@firebase)/,
exclude: /(node_modules)\/(@firebase)/,
loader: "babel-loader",
options: {
plugins: [

View File

@@ -20,29 +20,26 @@
"lintfix": "eslint --ext .ts,.js,.vue --ignore-path .gitignore . --fix",
"test": "jest"
},
"lint-staged": {
"*.{ts,js,vue}": "eslint",
"*.{css,scss,vue}": "stylelint"
},
"dependencies": {
"@apollo/client": "^3.4.10",
"@apollo/client": "^3.4.11",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.28.0",
"@nuxtjs/composition-api": "^0.29.0",
"@nuxtjs/gtm": "^2.4.0",
"@nuxtjs/i18n": "^7.0.3",
"@nuxtjs/robots": "^2.5.0",
"@nuxtjs/sitemap": "^2.4.0",
"@nuxtjs/toast": "^3.3.1",
"ace-builds": "^1.4.12",
"acorn": "^8.5.0",
"acorn-walk": "^8.2.0",
"axios": "^0.21.4",
"core-js": "^3.17.2",
"codemirror": "^5.62.3",
"codemirror-theme-github": "^1.0.0",
"core-js": "^3.17.3",
"esprima": "^4.0.1",
"firebase": "^9.0.1",
"firebase": "^9.0.2",
"fuse.js": "^6.4.6",
"graphql": "^15.5.0",
"graphql-language-service-interface": "^2.8.4",
"graphql-language-service-parser": "^1.9.2",
"json-loader": "^0.5.7",
"lodash": "^4.17.21",
"mustache": "^4.2.0",
@@ -65,7 +62,7 @@
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/preset-env": "^7.15.4",
"@babel/preset-env": "^7.15.6",
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@nuxt/types": "^2.15.8",
@@ -79,33 +76,34 @@
"@nuxtjs/stylelint-module": "^4.0.0",
"@nuxtjs/svg": "^0.2.0",
"@testing-library/jest-dom": "^5.14.1",
"@types/codemirror": "^5.60.2",
"@types/cookie": "^0.4.1",
"@types/esprima": "^4.0.3",
"@types/lodash": "^4.14.172",
"@types/splitpanes": "^2.2.1",
"@vue/runtime-dom": "^3.2.10",
"@vue/runtime-dom": "^3.2.11",
"@vue/test-utils": "^1.2.2",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^27.1.0",
"babel-jest": "^27.2.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-nuxt": ">=2.0.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.17.0",
"jest": "^27.1.0",
"jest": "^27.2.0",
"jest-serializer-vue": "^2.0.2",
"lint-staged": "^11.1.2",
"nuxt-windicss": "^1.2.3",
"prettier": "^2.3.2",
"nuxt-windicss": "^1.2.4",
"prettier": "^2.4.0",
"pretty-quick": "^3.1.1",
"raw-loader": "^4.0.2",
"sass": "^1.39.0",
"sass": "^1.40.1",
"sass-loader": "^10.2.0",
"stylelint": "^13.12.0",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"ts-jest": "^27.0.5",
"typescript": "^4.2",
"unplugin-vue2-script-setup": "^0.5.8",
"unplugin-vue2-script-setup": "^0.6.1",
"vue-jest": "^3.0.7",
"worker-loader": "^3.0.8"
}

View File

@@ -61,17 +61,12 @@
@click.native="collectionJSON = '[]'"
/>
</div>
<SmartAceEditor
<textarea-autosize
id="import-curl"
v-model="collectionJSON"
:lang="'json'"
:lint="false"
:options="{
maxLines: Infinity,
minLines: 16,
autoScrollEditorIntoView: true,
showPrintMargin: false,
useWorker: false,
}"
class="font-mono p-4 bg-primary"
autofocus
rows="8"
/>
<div
class="

View File

@@ -1,36 +1,30 @@
<template>
<div>
<Splitpanes
class="smart-splitter"
:dbl-click-splitter="false"
:horizontal="!(windowInnerWidth.x.value >= 768)"
<Splitpanes
class="smart-splitter"
:dbl-click-splitter="false"
:horizontal="!(windowInnerWidth.x.value >= 768)"
>
<Pane class="hide-scrollbar !overflow-auto">
<Splitpanes class="smart-splitter" :dbl-click-splitter="false" horizontal>
<Pane class="hide-scrollbar !overflow-auto">
<GraphqlRequest :conn="gqlConn" />
<GraphqlRequestOptions :conn="gqlConn" />
</Pane>
<Pane class="hide-scrollbar !overflow-auto">
<GraphqlResponse :conn="gqlConn" />
</Pane>
</Splitpanes>
</Pane>
<Pane
v-if="RIGHT_SIDEBAR"
max-size="35"
size="25"
min-size="20"
class="hide-scrollbar !overflow-auto"
>
<Pane class="hide-scrollbar !overflow-auto">
<Splitpanes
class="smart-splitter"
:dbl-click-splitter="false"
horizontal
>
<Pane class="hide-scrollbar !overflow-auto">
<GraphqlRequest :conn="gqlConn" />
<GraphqlRequestOptions :conn="gqlConn" />
</Pane>
<Pane class="hide-scrollbar !overflow-auto">
<GraphqlResponse :conn="gqlConn" />
</Pane>
</Splitpanes>
</Pane>
<Pane
v-if="RIGHT_SIDEBAR"
max-size="35"
size="25"
min-size="20"
class="hide-scrollbar !overflow-auto"
>
<GraphqlSidebar :conn="gqlConn" />
</Pane>
</Splitpanes>
</div>
<GraphqlSidebar :conn="gqlConn" />
</Pane>
</Splitpanes>
</template>
<script lang="ts">

View File

@@ -298,7 +298,7 @@
</div>
</div>
<div class="flex space-x-2 py-4 items-center">
<div class="flex flex-1 items-center relative">
<div class="flex flex-1 flex-col relative">
<input
id="url"
v-model="PROXY_URL"