Merge remote-tracking branch 'origin/feat/codemirror'
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -104,3 +104,6 @@ tests/*/screenshots
|
|||||||
|
|
||||||
# Tests videos
|
# Tests videos
|
||||||
tests/*/videos
|
tests/*/videos
|
||||||
|
|
||||||
|
# Andrew's crazy Volar shim generator
|
||||||
|
shims-volar.d.ts
|
||||||
|
|||||||
1
assets/icons/corner-down-left.svg
Normal file
1
assets/icons/corner-down-left.svg
Normal 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 |
@@ -17,7 +17,7 @@
|
|||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@apply bg-divider bg-clip-content;
|
@apply bg-divider bg-clip-content;
|
||||||
@apply rounded-full;
|
@apply rounded-full;
|
||||||
@apply border-solid border-4 border-transparent;
|
@apply border-solid border-transparent border-4;
|
||||||
@apply hover:(bg-dividerDark bg-clip-content);
|
@apply hover:(bg-dividerDark bg-clip-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +36,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
input::placeholder,
|
input::placeholder,
|
||||||
textarea::placeholder {
|
textarea::placeholder,
|
||||||
@apply text-secondaryDark;
|
.CodeMirror-empty {
|
||||||
|
@apply text-secondary;
|
||||||
@apply opacity-25;
|
@apply opacity-25;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,8 +117,8 @@ a {
|
|||||||
|
|
||||||
&.link {
|
&.link {
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@apply px-1 py-0.5;
|
@apply py-0.5 px-1;
|
||||||
@apply -mx-1 -my-0.5;
|
@apply -my-0.5 -mx-1;
|
||||||
@apply text-accent;
|
@apply text-accent;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply hover:text-accentDark;
|
@apply hover:text-accentDark;
|
||||||
@@ -198,7 +199,7 @@ hr {
|
|||||||
.textarea {
|
.textarea {
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
@apply px-4 py-2;
|
@apply py-2 px-4;
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply text-secondaryDark;
|
@apply text-secondaryDark;
|
||||||
@@ -293,7 +294,7 @@ input[type="checkbox"] {
|
|||||||
@apply cursor-pointer;
|
@apply cursor-pointer;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@apply border-2 border-divider;
|
@apply border-divider border-2;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply inline-flex;
|
@apply inline-flex;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@@ -347,6 +348,7 @@ input[type="checkbox"] {
|
|||||||
@apply justify-start;
|
@apply justify-start;
|
||||||
@apply shadow;
|
@apply shadow;
|
||||||
@apply font-medium;
|
@apply font-medium;
|
||||||
|
@apply transition;
|
||||||
|
|
||||||
font-size: var(--body-font-size);
|
font-size: var(--body-font-size);
|
||||||
line-height: var(--body-line-height);
|
line-height: var(--body-line-height);
|
||||||
@@ -358,7 +360,6 @@ input[type="checkbox"] {
|
|||||||
@apply ml-auto;
|
@apply ml-auto;
|
||||||
@apply last:ml-4;
|
@apply last:ml-4;
|
||||||
@apply sm:ml-8;
|
@apply sm:ml-8;
|
||||||
@apply transition;
|
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply text-current;
|
@apply text-current;
|
||||||
@apply normal-case;
|
@apply normal-case;
|
||||||
@@ -461,6 +462,32 @@ input[type="checkbox"] {
|
|||||||
@apply w-full;
|
@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) {
|
@media (max-width: 767px) {
|
||||||
main {
|
main {
|
||||||
margin-bottom: env(safe-area-inset-bottom);
|
margin-bottom: env(safe-area-inset-bottom);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mode === 'email'" class="flex flex-col space-y-2">
|
<div v-if="mode === 'email'" class="flex flex-col space-y-2">
|
||||||
<div class="flex items-center relative">
|
<div class="flex flex-col">
|
||||||
<input
|
<input
|
||||||
id="email"
|
id="email"
|
||||||
v-model="form.email"
|
v-model="form.email"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -33,45 +33,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, PropType } from "@nuxtjs/composition-api"
|
|
||||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||||
import { getCurrentStrategyID } from "~/helpers/network"
|
import { getCurrentStrategyID } from "~/helpers/network"
|
||||||
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
|
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
|
||||||
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
|
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
conn: GQLConnection
|
||||||
conn: {
|
}>()
|
||||||
type: Object as PropType<GQLConnection>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const connected = useReadonlyStream(props.conn.connected$, false)
|
|
||||||
const headers = useReadonlyStream(gqlHeaders$, [])
|
|
||||||
|
|
||||||
const url = useStream(gqlURL$, "", setGQLURL)
|
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||||
|
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||||
|
|
||||||
const onConnectClick = () => {
|
const url = useStream(gqlURL$, "", setGQLURL)
|
||||||
if (!connected.value) {
|
|
||||||
props.conn.connect(url.value, headers.value as any)
|
|
||||||
|
|
||||||
logHoppRequestRunToAnalytics({
|
const onConnectClick = () => {
|
||||||
platform: "graphql-schema",
|
if (!connected.value) {
|
||||||
strategy: getCurrentStrategyID(),
|
props.conn.connect(url.value, headers.value as any)
|
||||||
})
|
|
||||||
} else {
|
|
||||||
props.conn.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
logHoppRequestRunToAnalytics({
|
||||||
url,
|
platform: "graphql-schema",
|
||||||
connected,
|
strategy: getCurrentStrategyID(),
|
||||||
onConnectClick,
|
})
|
||||||
}
|
} else {
|
||||||
},
|
props.conn.disconnect()
|
||||||
})
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -42,9 +42,7 @@
|
|||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="`${$t(
|
:title="$t('action.prettify')"
|
||||||
'action.prettify'
|
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>P</kbd>`"
|
|
||||||
:svg="prettifyQueryIcon"
|
:svg="prettifyQueryIcon"
|
||||||
@click.native="prettifyQuery"
|
@click.native="prettifyQuery"
|
||||||
/>
|
/>
|
||||||
@@ -57,20 +55,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<GraphqlQueryEditor
|
<div ref="queryEditor"></div>
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</AppSection>
|
</AppSection>
|
||||||
</SmartTab>
|
</SmartTab>
|
||||||
|
|
||||||
@@ -108,19 +93,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartAceEditor
|
<div ref="variableEditor"></div>
|
||||||
ref="variableEditor"
|
|
||||||
v-model="variableString"
|
|
||||||
:lang="'json'"
|
|
||||||
:options="{
|
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
styles="border-b border-dividerLight"
|
|
||||||
/>
|
|
||||||
</AppSection>
|
</AppSection>
|
||||||
</SmartTab>
|
</SmartTab>
|
||||||
|
|
||||||
@@ -173,27 +146,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="bulkMode" class="flex">
|
<div v-if="bulkMode" ref="bulkEditor"></div>
|
||||||
<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-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
v-for="(header, index) in headers"
|
v-for="(header, index) in headers"
|
||||||
@@ -229,7 +182,9 @@
|
|||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
class="bg-transparent flex flex-1 py-2 px-4"
|
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}`"
|
:name="`value ${index}`"
|
||||||
:value="header.value"
|
:value="header.value"
|
||||||
autofocus
|
autofocus
|
||||||
@@ -311,17 +266,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { onMounted, ref, useContext, watch } from "@nuxtjs/composition-api"
|
||||||
defineComponent,
|
|
||||||
onMounted,
|
|
||||||
PropType,
|
|
||||||
ref,
|
|
||||||
useContext,
|
|
||||||
watch,
|
|
||||||
} from "@nuxtjs/composition-api"
|
|
||||||
import clone from "lodash/clone"
|
import clone from "lodash/clone"
|
||||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
import * as gql from "graphql"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
import {
|
import {
|
||||||
useNuxt,
|
useNuxt,
|
||||||
@@ -348,208 +296,207 @@ import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
|
|||||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||||
import { getCurrentStrategyID } from "~/helpers/network"
|
import { getCurrentStrategyID } from "~/helpers/network"
|
||||||
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
|
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({
|
const props = defineProps<{
|
||||||
props: {
|
conn: GQLConnection
|
||||||
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 bulkMode = ref(false)
|
const {
|
||||||
const bulkHeaders = ref("")
|
$toast,
|
||||||
|
app: { i18n },
|
||||||
|
} = useContext()
|
||||||
|
const t = i18n.t.bind(i18n)
|
||||||
|
const nuxt = useNuxt()
|
||||||
|
|
||||||
watch(bulkHeaders, () => {
|
const bulkMode = ref(false)
|
||||||
try {
|
const bulkHeaders = ref("")
|
||||||
const transformation = bulkHeaders.value.split("\n").map((item) => ({
|
|
||||||
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
watch(bulkHeaders, () => {
|
||||||
value: item.substring(item.indexOf(":") + 1).trim(),
|
try {
|
||||||
active: !item.trim().startsWith("//"),
|
const transformation = bulkHeaders.value.split("\n").map((item) => ({
|
||||||
}))
|
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
||||||
setGQLHeaders(transformation)
|
value: item.substring(item.indexOf(":") + 1).trim(),
|
||||||
} catch (e) {
|
active: !item.trim().startsWith("//"),
|
||||||
$toast.error(t("error.something_went_wrong").toString(), {
|
}))
|
||||||
icon: "error_outline",
|
setGQLHeaders(transformation)
|
||||||
})
|
} catch (e) {
|
||||||
console.error(e)
|
$toast.error(t("error.something_went_wrong").toString(), {
|
||||||
}
|
icon: "error_outline",
|
||||||
})
|
})
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const url = useReadonlyStream(gqlURL$, "")
|
const url = useReadonlyStream(gqlURL$, "")
|
||||||
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
|
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
|
||||||
const variableString = useStream(gqlVariables$, "", setGQLVariables)
|
const variableString = useStream(gqlVariables$, "", setGQLVariables)
|
||||||
const headers = useStream(gqlHeaders$, [], setGQLHeaders)
|
const headers = useStream(gqlHeaders$, [], setGQLHeaders)
|
||||||
|
|
||||||
const queryEditor = ref<any | null>(null)
|
const bulkEditor = ref<any | null>(null)
|
||||||
|
|
||||||
const copyQueryIcon = ref("copy")
|
useCodemirror(bulkEditor, bulkHeaders, {
|
||||||
const prettifyQueryIcon = ref("align-left")
|
extendedEditorConfig: {
|
||||||
const copyVariablesIcon = ref("copy")
|
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(
|
const queryEditor = ref<any | null>(null)
|
||||||
headers,
|
const schemaString = useReadonlyStream(props.conn.schema$, null)
|
||||||
() => {
|
|
||||||
if (
|
useCodemirror(queryEditor, gqlQueryString, {
|
||||||
(headers.value[headers.value.length - 1]?.key !== "" ||
|
extendedEditorConfig: {
|
||||||
headers.value[headers.value.length - 1]?.value !== "") &&
|
mode: "graphql",
|
||||||
headers.value.length
|
placeholder: t("request.query").toString(),
|
||||||
)
|
},
|
||||||
addRequestHeader()
|
linter: createGQLQueryLinter(schemaString),
|
||||||
},
|
completer: queryCompleter(schemaString),
|
||||||
{ deep: true }
|
})
|
||||||
|
|
||||||
|
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(() => {
|
$toast.success(t("state.finished_in", { duration }).toString(), {
|
||||||
if (!headers.value?.length) {
|
icon: "done",
|
||||||
addRequestHeader()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
response.value = `${e}. ${t("error.check_console_details")}`
|
||||||
|
nuxt.value.$loading.finish()
|
||||||
|
|
||||||
const copyQuery = () => {
|
$toast.error(`${e} ${t("error.f12_details").toString()}`, {
|
||||||
copyToClipboard(gqlQueryString.value)
|
icon: "error_outline",
|
||||||
copyQueryIcon.value = "check"
|
})
|
||||||
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = useStream(gqlResponse$, "", setGQLResponse)
|
logHoppRequestRunToAnalytics({
|
||||||
|
platform: "graphql-query",
|
||||||
|
strategy: getCurrentStrategyID(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const runQuery = async () => {
|
const hideRequestModal = () => {
|
||||||
const startTime = Date.now()
|
showSaveRequestModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
nuxt.value.$loading.start()
|
const prettifyQuery = () => {
|
||||||
response.value = t("state.loading").toString()
|
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 saveRequest = () => {
|
||||||
const runURL = clone(url.value)
|
showSaveRequestModal.value = true
|
||||||
const runHeaders = clone(headers.value)
|
}
|
||||||
const runQuery = clone(gqlQueryString.value)
|
|
||||||
const runVariables = clone(variableString.value)
|
|
||||||
|
|
||||||
const responseText = await props.conn.runQuery(
|
const copyVariables = () => {
|
||||||
runURL,
|
copyToClipboard(variableString.value)
|
||||||
runHeaders,
|
copyVariablesIcon.value = "check"
|
||||||
runQuery,
|
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
|
||||||
runVariables
|
}
|
||||||
)
|
|
||||||
const duration = Date.now() - startTime
|
|
||||||
|
|
||||||
nuxt.value.$loading.finish()
|
const addRequestHeader = () => {
|
||||||
|
addGQLHeader({
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
|
const removeRequestHeader = (index: number) => {
|
||||||
|
removeGQLHeader(index)
|
||||||
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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,6 +18,13 @@
|
|||||||
{{ $t("response.title") }}
|
{{ $t("response.title") }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex">
|
<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
|
<ButtonSecondary
|
||||||
ref="downloadResponse"
|
ref="downloadResponse"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -34,21 +41,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartAceEditor
|
<div v-if="responseString" ref="schemaEditor"></div>
|
||||||
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
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="
|
class="
|
||||||
@@ -60,35 +53,21 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="flex space-x-2 pb-4">
|
<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">
|
<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") }}
|
{{ $t("shortcut.general.command_menu") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex flex-1 items-center">
|
<span class="flex flex-1 items-center">
|
||||||
{{ $t("shortcut.general.help_menu") }}
|
{{ $t("shortcut.general.help_menu") }}
|
||||||
</span> -->
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-4">
|
||||||
<div class="flex">
|
<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>
|
<span class="shortcut-key">/</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<span class="shortcut-key">?</span>
|
<span class="shortcut-key">?</span>
|
||||||
</div> -->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
@@ -103,77 +82,66 @@
|
|||||||
</AppSection>
|
</AppSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
|
||||||
defineComponent,
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
PropType,
|
|
||||||
ref,
|
|
||||||
useContext,
|
|
||||||
} from "@nuxtjs/composition-api"
|
|
||||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
|
||||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||||
import { gqlResponse$ } from "~/newstore/GQLSession"
|
import { gqlResponse$ } from "~/newstore/GQLSession"
|
||||||
|
|
||||||
export default defineComponent({
|
const {
|
||||||
props: {
|
$toast,
|
||||||
conn: {
|
app: { i18n },
|
||||||
type: Object as PropType<GQLConnection>,
|
} = useContext()
|
||||||
required: true,
|
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,
|
||||||
},
|
},
|
||||||
},
|
linter: null,
|
||||||
setup() {
|
completer: null,
|
||||||
const {
|
})
|
||||||
$toast,
|
)
|
||||||
app: { i18n },
|
|
||||||
} = useContext()
|
|
||||||
const t = i18n.t.bind(i18n)
|
|
||||||
|
|
||||||
const responseString = useReadonlyStream(gqlResponse$, "")
|
const downloadResponseIcon = ref("download")
|
||||||
|
const copyResponseIcon = ref("copy")
|
||||||
|
|
||||||
const downloadResponseIcon = ref("download")
|
const copyResponse = () => {
|
||||||
const copyResponseIcon = ref("copy")
|
copyToClipboard(responseString.value!)
|
||||||
|
copyResponseIcon.value = "check"
|
||||||
|
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
const copyResponse = () => {
|
const downloadResponse = () => {
|
||||||
copyToClipboard(responseString.value!)
|
const dataToWrite = responseString.value
|
||||||
copyResponseIcon.value = "check"
|
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||||
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
|
const a = document.createElement("a")
|
||||||
}
|
const url = URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
const downloadResponse = () => {
|
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
||||||
const dataToWrite = responseString.value
|
document.body.appendChild(a)
|
||||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
a.click()
|
||||||
const a = document.createElement("a")
|
downloadResponseIcon.value = "check"
|
||||||
const url = URL.createObjectURL(file)
|
$toast.success(t("state.download_started").toString(), {
|
||||||
a.href = url
|
icon: "downloading",
|
||||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
})
|
||||||
document.body.appendChild(a)
|
setTimeout(() => {
|
||||||
a.click()
|
document.body.removeChild(a)
|
||||||
downloadResponseIcon.value = "check"
|
URL.revokeObjectURL(url)
|
||||||
$toast.success(t("state.download_started").toString(), {
|
downloadResponseIcon.value = "download"
|
||||||
icon: "downloading",
|
}, 1000)
|
||||||
})
|
}
|
||||||
setTimeout(() => {
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
downloadResponseIcon.value = "download"
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
responseString,
|
|
||||||
|
|
||||||
downloadResponseIcon,
|
|
||||||
copyResponseIcon,
|
|
||||||
|
|
||||||
downloadResponse,
|
|
||||||
copyResponse,
|
|
||||||
|
|
||||||
getSpecialKey: getPlatformSpecialKey,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -149,6 +149,13 @@
|
|||||||
:title="$t('app.wiki')"
|
:title="$t('app.wiki')"
|
||||||
svg="help-circle"
|
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
|
<ButtonSecondary
|
||||||
ref="downloadSchema"
|
ref="downloadSchema"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -165,20 +172,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartAceEditor
|
<div v-if="schemaString" ref="schemaEditor"></div>
|
||||||
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
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="
|
class="
|
||||||
@@ -200,17 +194,17 @@
|
|||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
defineComponent,
|
|
||||||
nextTick,
|
nextTick,
|
||||||
PropType,
|
reactive,
|
||||||
ref,
|
ref,
|
||||||
useContext,
|
useContext,
|
||||||
} from "@nuxtjs/composition-api"
|
} from "@nuxtjs/composition-api"
|
||||||
import { GraphQLField, GraphQLType } from "graphql"
|
import { GraphQLField, GraphQLType } from "graphql"
|
||||||
import { map } from "rxjs/operators"
|
import { map } from "rxjs/operators"
|
||||||
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||||
import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
|
import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
@@ -222,6 +216,7 @@ import {
|
|||||||
setGQLURL,
|
setGQLURL,
|
||||||
setGQLVariables,
|
setGQLVariables,
|
||||||
} from "~/newstore/GQLSession"
|
} from "~/newstore/GQLSession"
|
||||||
|
import "~/helpers/editor/modes/graphql"
|
||||||
|
|
||||||
function isTextFoundInGraphqlFieldObject(
|
function isTextFoundInGraphqlFieldObject(
|
||||||
text: string,
|
text: string,
|
||||||
@@ -285,186 +280,168 @@ type GQLHistoryEntry = {
|
|||||||
variables: string
|
variables: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
conn: GQLConnection
|
||||||
conn: {
|
}>()
|
||||||
type: Object as PropType<GQLConnection>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const {
|
|
||||||
$toast,
|
|
||||||
app: { i18n },
|
|
||||||
} = useContext()
|
|
||||||
const t = i18n.t.bind(i18n)
|
|
||||||
|
|
||||||
const queryFields = useReadonlyStream(
|
const {
|
||||||
props.conn.queryFields$.pipe(map((x) => x ?? [])),
|
$toast,
|
||||||
[]
|
app: { i18n },
|
||||||
)
|
} = useContext()
|
||||||
const mutationFields = useReadonlyStream(
|
const t = i18n.t.bind(i18n)
|
||||||
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 downloadSchemaIcon = ref("download")
|
const queryFields = useReadonlyStream(
|
||||||
const copySchemaIcon = ref("copy")
|
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 subscriptionFields = useReadonlyStream(
|
||||||
const typesTab = ref<any | null>(null)
|
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const filteredQueryFields = computed(() => {
|
const graphqlTypes = useReadonlyStream(
|
||||||
return getFilteredGraphqlFields(
|
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
|
||||||
graphqlFieldsFilterText.value,
|
[]
|
||||||
queryFields.value as any
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredMutationFields = computed(() => {
|
const downloadSchemaIcon = ref("download")
|
||||||
return getFilteredGraphqlFields(
|
const copySchemaIcon = ref("copy")
|
||||||
graphqlFieldsFilterText.value,
|
|
||||||
mutationFields.value as any
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredSubscriptionFields = computed(() => {
|
const graphqlFieldsFilterText = ref("")
|
||||||
return getFilteredGraphqlFields(
|
|
||||||
graphqlFieldsFilterText.value,
|
|
||||||
subscriptionFields.value as any
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const filteredGraphqlTypes = computed(() => {
|
const gqlTabs = ref<any | null>(null)
|
||||||
return getFilteredGraphqlTypes(
|
const typesTab = ref<any | null>(null)
|
||||||
graphqlFieldsFilterText.value,
|
|
||||||
graphqlTypes.value as any
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
|
const filteredQueryFields = computed(() => {
|
||||||
if (!graphqlFieldsFilterText.value) return false
|
return getFilteredGraphqlFields(
|
||||||
|
graphqlFieldsFilterText.value,
|
||||||
return isTextFoundInGraphqlFieldObject(
|
queryFields.value as any
|
||||||
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 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>
|
</script>
|
||||||
|
|||||||
@@ -38,31 +38,25 @@
|
|||||||
{{ t("request.generated_code") }}
|
{{ t("request.generated_code") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<SmartAceEditor
|
<div
|
||||||
v-if="codegenType"
|
v-if="codegenType"
|
||||||
ref="generatedCode"
|
ref="generatedCode"
|
||||||
:value="requestCode"
|
class="border border-dividerLight rounded"
|
||||||
:lang="codegens.find((x) => x.id === codegenType).language"
|
></div>
|
||||||
:options="{
|
|
||||||
maxLines: 16,
|
|
||||||
minLines: 8,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
readOnly: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
styles="border rounded border-dividerLight"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<ButtonPrimary
|
<span class="flex">
|
||||||
ref="copyRequestCode"
|
<ButtonPrimary
|
||||||
:label="t('action.copy')"
|
:label="t('action.copy').toString()"
|
||||||
:svg="copyIcon"
|
:svg="copyIcon"
|
||||||
@click.native="copyRequestCode"
|
@click.native="copyRequestCode"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary :label="t('action.dismiss')" @click.native="hideModal" />
|
<ButtonSecondary
|
||||||
|
:label="t('action.dismiss').toString()"
|
||||||
|
@click.native="hideModal"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</SmartModal>
|
</SmartModal>
|
||||||
</template>
|
</template>
|
||||||
@@ -70,6 +64,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, useContext, watch } from "@nuxtjs/composition-api"
|
import { computed, ref, useContext, watch } from "@nuxtjs/composition-api"
|
||||||
import { codegens, generateCodegenContext } from "~/helpers/codegen/codegen"
|
import { codegens, generateCodegenContext } from "~/helpers/codegen/codegen"
|
||||||
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
import { getEffectiveRESTRequest } from "~/helpers/utils/EffectiveURL"
|
import { getEffectiveRESTRequest } from "~/helpers/utils/EffectiveURL"
|
||||||
import { getCurrentEnvironment } from "~/newstore/environments"
|
import { getCurrentEnvironment } from "~/newstore/environments"
|
||||||
@@ -106,6 +101,17 @@ const requestCode = computed(() => {
|
|||||||
.generator(generateCodegenContext(effectiveRequest))
|
.generator(generateCodegenContext(effectiveRequest))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const generatedCode = ref<any | null>(null)
|
||||||
|
|
||||||
|
useCodemirror(generatedCode, requestCode, {
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "text/plain",
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
linter: null,
|
||||||
|
completer: null,
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(goingToShow) => {
|
(goingToShow) => {
|
||||||
|
|||||||
@@ -47,27 +47,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="bulkMode" class="flex">
|
<div v-if="bulkMode" ref="bulkEditor"></div>
|
||||||
<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-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
v-for="(header, index) in headers$"
|
v-for="(header, index) in headers$"
|
||||||
@@ -193,96 +173,86 @@
|
|||||||
</AppSection>
|
</AppSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, useContext, watch } from "@nuxtjs/composition-api"
|
||||||
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import {
|
import {
|
||||||
defineComponent,
|
|
||||||
ref,
|
|
||||||
useContext,
|
|
||||||
watch,
|
|
||||||
} from "@nuxtjs/composition-api"
|
|
||||||
import {
|
|
||||||
restHeaders$,
|
|
||||||
addRESTHeader,
|
addRESTHeader,
|
||||||
updateRESTHeader,
|
|
||||||
deleteRESTHeader,
|
|
||||||
deleteAllRESTHeaders,
|
deleteAllRESTHeaders,
|
||||||
|
deleteRESTHeader,
|
||||||
|
restHeaders$,
|
||||||
setRESTHeaders,
|
setRESTHeaders,
|
||||||
|
updateRESTHeader,
|
||||||
} from "~/newstore/RESTSession"
|
} from "~/newstore/RESTSession"
|
||||||
import { commonHeaders } from "~/helpers/headers"
|
import { commonHeaders } from "~/helpers/headers"
|
||||||
import { useSetting } from "~/newstore/settings"
|
import { useSetting } from "~/newstore/settings"
|
||||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||||
import { HoppRESTHeader } from "~/helpers/types/HoppRESTRequest"
|
import { HoppRESTHeader } from "~/helpers/types/HoppRESTRequest"
|
||||||
|
|
||||||
export default defineComponent({
|
const {
|
||||||
setup() {
|
$toast,
|
||||||
const {
|
app: { i18n },
|
||||||
$toast,
|
} = useContext()
|
||||||
app: { i18n },
|
const t = i18n.t.bind(i18n)
|
||||||
} = useContext()
|
|
||||||
const t = i18n.t.bind(i18n)
|
|
||||||
|
|
||||||
const bulkMode = ref(false)
|
const bulkMode = ref(false)
|
||||||
const bulkHeaders = ref("")
|
const bulkHeaders = ref("")
|
||||||
|
const bulkEditor = ref<any | null>(null)
|
||||||
|
|
||||||
watch(bulkHeaders, () => {
|
useCodemirror(bulkEditor, bulkHeaders, {
|
||||||
try {
|
extendedEditorConfig: {
|
||||||
const transformation = bulkHeaders.value.split("\n").map((item) => ({
|
mode: "text/x-yaml",
|
||||||
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
placeholder: t("state.bulk_mode_placeholder").toString(),
|
||||||
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()
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<SmartModal v-if="show" :title="$t('import.curl')" @close="hideModal">
|
<SmartModal
|
||||||
|
v-if="show"
|
||||||
|
:title="$t('import.curl').toString()"
|
||||||
|
@close="hideModal"
|
||||||
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col px-2">
|
<div class="flex flex-col px-2">
|
||||||
<textarea-autosize
|
<div ref="curlEditor" class="border border-dividerLight rounded"></div>
|
||||||
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<span>
|
<span class="flex">
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
:label="$t('import.title')"
|
:label="$t('import.title').toString()"
|
||||||
@click.native="handleImport"
|
@click.native="handleImport"
|
||||||
/>
|
/>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
:label="$t('action.cancel')"
|
:label="$t('action.cancel').toString()"
|
||||||
@click.native="hideModal"
|
@click.native="hideModal"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -30,108 +24,114 @@
|
|||||||
</SmartModal>
|
</SmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { ref, useContext } from "@nuxtjs/composition-api"
|
||||||
import parseCurlCommand from "~/helpers/curlparser"
|
import parseCurlCommand from "~/helpers/curlparser"
|
||||||
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import {
|
import {
|
||||||
HoppRESTHeader,
|
HoppRESTHeader,
|
||||||
HoppRESTParam,
|
HoppRESTParam,
|
||||||
makeRESTRequest,
|
makeRESTRequest,
|
||||||
} from "~/helpers/types/HoppRESTRequest"
|
} from "~/helpers/types/HoppRESTRequest"
|
||||||
import { setRESTRequest } from "~/newstore/RESTSession"
|
import { setRESTRequest } from "~/newstore/RESTSession"
|
||||||
|
import "codemirror/mode/shell/shell"
|
||||||
|
|
||||||
export default defineComponent({
|
const {
|
||||||
props: {
|
$toast,
|
||||||
show: Boolean,
|
app: { i18n },
|
||||||
},
|
} = useContext()
|
||||||
emits: ["hide-modal"],
|
const t = i18n.t.bind(i18n)
|
||||||
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]!
|
|
||||||
|
|
||||||
if (Array.isArray(val)) {
|
const curl = ref("")
|
||||||
val.forEach((value) => {
|
|
||||||
params.push({
|
const curlEditor = ref<any | null>(null)
|
||||||
key,
|
|
||||||
value,
|
useCodemirror(curlEditor, curl, {
|
||||||
active: true,
|
extendedEditorConfig: {
|
||||||
})
|
mode: "application/x-sh",
|
||||||
})
|
placeholder: t("request.enter_curl").toString(),
|
||||||
} else {
|
},
|
||||||
params.push({
|
linter: null,
|
||||||
key,
|
completer: null,
|
||||||
value: val!,
|
})
|
||||||
active: true,
|
|
||||||
})
|
defineProps<{ show: boolean }>()
|
||||||
}
|
|
||||||
}
|
const emit = defineEmits<{
|
||||||
}
|
(e: "hide-modal"): void
|
||||||
if (parsedCurl.headers) {
|
}>()
|
||||||
for (const key of Object.keys(parsedCurl.headers)) {
|
|
||||||
headers.push({
|
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,
|
key,
|
||||||
value: parsedCurl.headers[key],
|
value,
|
||||||
active: true,
|
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: "",
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
)
|
} else {
|
||||||
} catch (e) {
|
params.push({
|
||||||
console.error(e)
|
key,
|
||||||
this.$toast.error(this.$t("error.curl_invalid_format").toString(), {
|
value: val!,
|
||||||
icon: "error_outline",
|
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>
|
</script>
|
||||||
|
|||||||
@@ -47,27 +47,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="bulkMode" class="flex">
|
<div v-if="bulkMode" ref="bulkEditor"></div>
|
||||||
<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-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
v-for="(param, index) in params$"
|
v-for="(param, index) in params$"
|
||||||
@@ -96,7 +76,7 @@
|
|||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
class="bg-transparent flex flex-1 py-2 px-4"
|
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"
|
:name="'param' + index"
|
||||||
:value="param.key"
|
:value="param.key"
|
||||||
autofocus
|
autofocus
|
||||||
@@ -130,7 +110,7 @@
|
|||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
class="bg-transparent flex flex-1 py-2 px-4"
|
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"
|
:name="'value' + index"
|
||||||
:value="param.value"
|
:value="param.value"
|
||||||
@change="
|
@change="
|
||||||
@@ -202,13 +182,9 @@
|
|||||||
</AppSection>
|
</AppSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { ref, useContext, watch } from "@nuxtjs/composition-api"
|
||||||
defineComponent,
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
ref,
|
|
||||||
useContext,
|
|
||||||
watch,
|
|
||||||
} from "@nuxtjs/composition-api"
|
|
||||||
import { HoppRESTParam } from "~/helpers/types/HoppRESTRequest"
|
import { HoppRESTParam } from "~/helpers/types/HoppRESTRequest"
|
||||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||||
import {
|
import {
|
||||||
@@ -220,72 +196,74 @@ import {
|
|||||||
setRESTParams,
|
setRESTParams,
|
||||||
} from "~/newstore/RESTSession"
|
} from "~/newstore/RESTSession"
|
||||||
import { useSetting } from "~/newstore/settings"
|
import { useSetting } from "~/newstore/settings"
|
||||||
|
import "codemirror/mode/yaml/yaml"
|
||||||
|
|
||||||
export default defineComponent({
|
const {
|
||||||
setup() {
|
$toast,
|
||||||
const {
|
app: { i18n },
|
||||||
$toast,
|
} = useContext()
|
||||||
app: { i18n },
|
const t = i18n.t.bind(i18n)
|
||||||
} = useContext()
|
|
||||||
const t = i18n.t.bind(i18n)
|
|
||||||
|
|
||||||
const bulkMode = ref(false)
|
const bulkMode = ref(false)
|
||||||
const bulkParams = ref("")
|
const bulkParams = ref("")
|
||||||
|
|
||||||
watch(bulkParams, () => {
|
watch(bulkParams, () => {
|
||||||
try {
|
try {
|
||||||
const transformation = bulkParams.value.split("\n").map((item) => ({
|
const transformation = bulkParams.value.split("\n").map((item) => ({
|
||||||
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
||||||
value: item.substring(item.indexOf(":") + 1).trim(),
|
value: item.substring(item.indexOf(":") + 1).trim(),
|
||||||
active: !item.trim().startsWith("//"),
|
active: !item.trim().startsWith("//"),
|
||||||
}))
|
}))
|
||||||
setRESTParams(transformation)
|
setRESTParams(transformation)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$toast.error(t("error.something_went_wrong").toString(), {
|
$toast.error(t("error.something_went_wrong").toString(), {
|
||||||
icon: "error_outline",
|
icon: "error_outline",
|
||||||
})
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
console.error(e)
|
||||||
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()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
|||||||
@@ -24,6 +24,13 @@
|
|||||||
:title="$t('app.wiki')"
|
:title="$t('app.wiki')"
|
||||||
svg="help-circle"
|
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
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('action.clear')"
|
:title="$t('action.clear')"
|
||||||
@@ -34,17 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="border-b border-dividerLight flex">
|
<div class="border-b border-dividerLight flex">
|
||||||
<div class="border-r border-dividerLight w-2/3">
|
<div class="border-r border-dividerLight w-2/3">
|
||||||
<SmartJsEditor
|
<div ref="preRrequestEditor"></div>
|
||||||
v-model="preRequestScript"
|
|
||||||
:options="{
|
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
complete-mode="pre"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
@@ -84,29 +81,44 @@
|
|||||||
</AppSection>
|
</AppSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
|
||||||
import { usePreRequestScript } from "~/newstore/RESTSession"
|
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({
|
const {
|
||||||
setup() {
|
app: { i18n },
|
||||||
const preRequestScript = usePreRequestScript()
|
} = useContext()
|
||||||
|
const t = i18n.t.bind(i18n)
|
||||||
|
|
||||||
const useSnippet = (script: string) => {
|
const preRequestScript = usePreRequestScript()
|
||||||
preRequestScript.value += script
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearContent = () => {
|
const preRrequestEditor = ref<any | null>(null)
|
||||||
preRequestScript.value = ""
|
const linewrapEnabled = ref(true)
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
useCodemirror(
|
||||||
preRequestScript,
|
preRrequestEditor,
|
||||||
snippets: preRequestScriptSnippets,
|
preRequestScript,
|
||||||
useSnippet,
|
reactive({
|
||||||
clearContent,
|
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>
|
</script>
|
||||||
|
|||||||
@@ -24,6 +24,13 @@
|
|||||||
:title="$t('app.wiki')"
|
:title="$t('app.wiki')"
|
||||||
svg="help-circle"
|
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
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('action.clear')"
|
:title="$t('action.clear')"
|
||||||
@@ -55,82 +62,87 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div ref="rawBodyParameters"></div>
|
||||||
<SmartAceEditor
|
|
||||||
v-model="rawParamsBody"
|
|
||||||
:lang="rawInputEditorLang"
|
|
||||||
:options="{
|
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
styles="border-b border-dividerLight"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { computed, reactive, ref, useContext } from "@nuxtjs/composition-api"
|
||||||
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { getEditorLangForMimeType } from "~/helpers/editorutils"
|
import { getEditorLangForMimeType } from "~/helpers/editorutils"
|
||||||
import { pluckRef } from "~/helpers/utils/composables"
|
import { pluckRef } from "~/helpers/utils/composables"
|
||||||
import { useRESTRequestBody } from "~/newstore/RESTSession"
|
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({
|
const props = defineProps<{
|
||||||
props: {
|
contentType: string
|
||||||
contentType: {
|
}>()
|
||||||
type: String,
|
|
||||||
required: true,
|
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(),
|
||||||
},
|
},
|
||||||
},
|
linter: null,
|
||||||
setup() {
|
completer: null,
|
||||||
return {
|
})
|
||||||
rawParamsBody: pluckRef(useRESTRequestBody(), "body"),
|
)
|
||||||
prettifyIcon: "align-left",
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
},
|
reader.readAsText(file)
|
||||||
computed: {
|
$toast.success(t("state.file_imported").toString(), {
|
||||||
rawInputEditorLang() {
|
icon: "attach_file",
|
||||||
return getEditorLangForMimeType(this.contentType)
|
})
|
||||||
},
|
} else {
|
||||||
},
|
$toast.error(t("action.choose_file").toString(), {
|
||||||
methods: {
|
icon: "attach_file",
|
||||||
clearContent() {
|
})
|
||||||
this.rawParamsBody = ""
|
}
|
||||||
},
|
}
|
||||||
uploadPayload() {
|
const prettifyRequestBody = () => {
|
||||||
const file = this.$refs.payload.files[0]
|
try {
|
||||||
if (file !== undefined && file !== null) {
|
const jsonObj = JSON.parse(rawParamsBody.value)
|
||||||
const reader = new FileReader()
|
rawParamsBody.value = JSON.stringify(jsonObj, null, 2)
|
||||||
reader.onload = ({ target }) => {
|
prettifyIcon.value = "check"
|
||||||
this.rawParamsBody = target.result
|
setTimeout(() => (prettifyIcon.value = "align-left"), 1000)
|
||||||
}
|
} catch (e) {
|
||||||
reader.readAsText(file)
|
console.error(e)
|
||||||
this.$toast.success(this.$t("state.file_imported"), {
|
$toast.error(`${t("error.json_prettify_invalid_body")}`, {
|
||||||
icon: "attach_file",
|
icon: "error_outline",
|
||||||
})
|
})
|
||||||
} 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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,19 +10,19 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="flex space-x-2 pb-4">
|
<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">
|
<span class="flex flex-1 items-center">
|
||||||
{{ $t("shortcut.request.send_request") }}
|
{{ $t("shortcut.request.send_request") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex flex-1 items-center">
|
<span class="flex flex-1 items-center">
|
||||||
{{ $t("shortcut.general.show_all") }}
|
{{ $t("shortcut.general.show_all") }}
|
||||||
</span>
|
</span>
|
||||||
<!-- <span class="flex flex-1 items-center">
|
<span class="flex flex-1 items-center">
|
||||||
{{ $t("shortcut.general.command_menu") }}
|
{{ $t("shortcut.general.command_menu") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex flex-1 items-center">
|
<span class="flex flex-1 items-center">
|
||||||
{{ $t("shortcut.general.help_menu") }}
|
{{ $t("shortcut.general.help_menu") }}
|
||||||
</span> -->
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-4">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -33,12 +33,12 @@
|
|||||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
||||||
<span class="shortcut-key">K</span>
|
<span class="shortcut-key">K</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="flex">
|
<div class="flex">
|
||||||
<span class="shortcut-key">/</span>
|
<span class="shortcut-key">/</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<span class="shortcut-key">?</span>
|
<span class="shortcut-key">?</span>
|
||||||
</div> -->
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
@@ -102,26 +102,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { computed } from "@nuxtjs/composition-api"
|
||||||
import findStatusGroup from "~/helpers/findStatusGroup"
|
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({
|
const props = defineProps<{
|
||||||
props: {
|
response: HoppRESTResponse
|
||||||
response: {
|
}>()
|
||||||
type: Object,
|
|
||||||
default: () => null,
|
const statusCategory = computed(() => {
|
||||||
},
|
if (
|
||||||
},
|
props.response.type === "loading" ||
|
||||||
computed: {
|
props.response.type === "network_fail"
|
||||||
statusCategory() {
|
)
|
||||||
return findStatusGroup(this.response.statusCode)
|
return ""
|
||||||
},
|
return findStatusGroup(props.response.statusCode)
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getSpecialKey: getPlatformSpecialKey,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,13 @@
|
|||||||
:title="$t('app.wiki')"
|
:title="$t('app.wiki')"
|
||||||
svg="help-circle"
|
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
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="$t('action.clear')"
|
:title="$t('action.clear')"
|
||||||
@@ -34,17 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="border-b border-dividerLight flex">
|
<div class="border-b border-dividerLight flex">
|
||||||
<div class="border-r border-dividerLight w-2/3">
|
<div class="border-r border-dividerLight w-2/3">
|
||||||
<SmartJsEditor
|
<div ref="testScriptEditor"></div>
|
||||||
v-model="testScript"
|
|
||||||
:options="{
|
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
complete-mode="test"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
@@ -85,11 +82,38 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
|
||||||
import { useTestScript } from "~/newstore/RESTSession"
|
import { useTestScript } from "~/newstore/RESTSession"
|
||||||
import testSnippets from "~/helpers/testSnippets"
|
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 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) => {
|
const useSnippet = (script: string) => {
|
||||||
testScript.value += script
|
testScript.value += script
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
group-hover:text-secondaryDark
|
group-hover:text-secondaryDark
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<span class="rounded select-all truncate">
|
<span class="rounded-sm select-all truncate">
|
||||||
{{ header.key }}
|
{{ header.key }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
group-hover:text-secondaryDark
|
group-hover:text-secondaryDark
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<span class="rounded select-all truncate">
|
<span class="rounded-sm select-all truncate">
|
||||||
{{ header.value }}
|
{{ header.value }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -17,6 +17,14 @@
|
|||||||
{{ $t("response.body") }}
|
{{ $t("response.body") }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex">
|
<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
|
<ButtonSecondary
|
||||||
v-if="response.body"
|
v-if="response.body"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -44,110 +52,131 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div v-show="!previewEnabled" ref="htmlResponse"></div>
|
||||||
<SmartAceEditor
|
<iframe
|
||||||
:value="responseBodyText"
|
v-show="previewEnabled"
|
||||||
:lang="'html'"
|
ref="previewFrame"
|
||||||
:options="{
|
class="covers-response"
|
||||||
maxLines: Infinity,
|
src="about:blank"
|
||||||
minLines: 16,
|
></iframe>
|
||||||
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
|
||||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
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({
|
const props = defineProps<{
|
||||||
mixins: [TextContentRendererMixin],
|
response: HoppRESTResponse
|
||||||
props: {
|
}>()
|
||||||
response: { type: Object, default: () => {} },
|
|
||||||
},
|
const {
|
||||||
data() {
|
$toast,
|
||||||
return {
|
app: { i18n },
|
||||||
downloadIcon: "download",
|
} = useContext()
|
||||||
copyIcon: "copy",
|
const t = i18n.t.bind(i18n)
|
||||||
previewEnabled: false,
|
|
||||||
}
|
const responseBodyText = computed(() => {
|
||||||
},
|
if (
|
||||||
methods: {
|
props.response.type === "loading" ||
|
||||||
downloadResponse() {
|
props.response.type === "network_fail"
|
||||||
const dataToWrite = this.responseBodyText
|
)
|
||||||
const file = new Blob([dataToWrite], { type: "text/html" })
|
return ""
|
||||||
const a = document.createElement("a")
|
if (typeof props.response.body === "string") return props.response.body
|
||||||
const url = URL.createObjectURL(file)
|
else {
|
||||||
a.href = url
|
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||||
// TODO get uri from meta
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
return res.replace(/\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 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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.covers-response {
|
.covers-response {
|
||||||
@apply absolute;
|
|
||||||
@apply inset-0;
|
|
||||||
@apply bg-white;
|
@apply bg-white;
|
||||||
|
@apply min-h-64;
|
||||||
@apply h-full;
|
@apply h-full;
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
@apply border;
|
@apply border;
|
||||||
@apply border-dividerLight;
|
@apply border-dividerLight;
|
||||||
|
@apply z-5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -27,12 +27,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex relative">
|
<img
|
||||||
<img
|
class="border-b border-dividerLight flex max-w-full flex-1"
|
||||||
class="border-b border-dividerLight flex max-w-full flex-1"
|
:src="imageSource"
|
||||||
:src="imageSource"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,18 @@
|
|||||||
justify-between
|
justify-between
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<label class="font-semibold text-secondaryLight">
|
<label class="font-semibold text-secondaryLight">{{
|
||||||
{{ $t("response.body") }}
|
$t("response.body")
|
||||||
</label>
|
}}</label>
|
||||||
<div class="flex">
|
<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
|
<ButtonSecondary
|
||||||
v-if="response.body"
|
v-if="response.body"
|
||||||
ref="downloadResponse"
|
ref="downloadResponse"
|
||||||
@@ -35,89 +43,237 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div ref="jsonResponse"></div>
|
||||||
<SmartAceEditor
|
<div
|
||||||
:value="jsonBodyText"
|
v-if="outlinePath"
|
||||||
:lang="'json'"
|
class="
|
||||||
:provide-outline="true"
|
bg-primaryLight
|
||||||
:options="{
|
border-t border-dividerLight
|
||||||
maxLines: Infinity,
|
flex flex-nowrap flex-1
|
||||||
minLines: 16,
|
px-2
|
||||||
autoScrollEditorIntoView: true,
|
bottom-0
|
||||||
readOnly: true,
|
z-10
|
||||||
showPrintMargin: false,
|
sticky
|
||||||
useWorker: false,
|
overflow-auto
|
||||||
}"
|
hide-scrollbar
|
||||||
styles="border-b border-dividerLight"
|
"
|
||||||
/>
|
>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
|
||||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
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({
|
const props = defineProps<{
|
||||||
mixins: [TextContentRendererMixin],
|
response: HoppRESTResponse
|
||||||
props: {
|
}>()
|
||||||
response: { type: Object, default: () => {} },
|
|
||||||
},
|
const {
|
||||||
data() {
|
$toast,
|
||||||
return {
|
app: { i18n },
|
||||||
downloadIcon: "download",
|
} = useContext()
|
||||||
copyIcon: "copy",
|
const t = i18n.t.bind(i18n)
|
||||||
}
|
|
||||||
},
|
const responseBodyText = computed(() => {
|
||||||
computed: {
|
if (
|
||||||
jsonBodyText() {
|
props.response.type === "loading" ||
|
||||||
try {
|
props.response.type === "network_fail"
|
||||||
return JSON.stringify(JSON.parse(this.responseBodyText), null, 2)
|
)
|
||||||
} catch (e) {
|
return ""
|
||||||
// Most probs invalid JSON was returned, so drop prettification (should we warn ?)
|
if (typeof props.response.body === "string") return props.response.body
|
||||||
return this.responseBodyText
|
else {
|
||||||
}
|
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||||
},
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
responseType() {
|
return res.replace(/\0+$/, "")
|
||||||
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 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>
|
</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>
|
||||||
|
|||||||
@@ -17,6 +17,14 @@
|
|||||||
{{ $t("response.body") }}
|
{{ $t("response.body") }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex">
|
<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
|
<ButtonSecondary
|
||||||
v-if="response.body"
|
v-if="response.body"
|
||||||
ref="downloadResponse"
|
ref="downloadResponse"
|
||||||
@@ -35,80 +43,96 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div ref="rawResponse"></div>
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { ref, useContext, computed, reactive } from "@nuxtjs/composition-api"
|
||||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
mixins: [TextContentRendererMixin],
|
response: HoppRESTResponse
|
||||||
props: {
|
}>()
|
||||||
response: { type: Object, default: () => {} },
|
|
||||||
},
|
const {
|
||||||
data() {
|
$toast,
|
||||||
return {
|
app: { i18n },
|
||||||
downloadIcon: "download",
|
} = useContext()
|
||||||
copyIcon: "copy",
|
const t = i18n.t.bind(i18n)
|
||||||
}
|
|
||||||
},
|
const responseBodyText = computed(() => {
|
||||||
computed: {
|
if (
|
||||||
responseType() {
|
props.response.type === "loading" ||
|
||||||
return (
|
props.response.type === "network_fail"
|
||||||
this.response.headers.find(
|
)
|
||||||
(h) => h.key.toLowerCase() === "content-type"
|
return ""
|
||||||
).value || ""
|
if (typeof props.response.body === "string") return props.response.body
|
||||||
)
|
else {
|
||||||
.split(";")[0]
|
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||||
.toLowerCase()
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
},
|
return res.replace(/\0+$/, "")
|
||||||
},
|
}
|
||||||
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 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>
|
</script>
|
||||||
|
|||||||
@@ -17,6 +17,14 @@
|
|||||||
{{ $t("response.body") }}
|
{{ $t("response.body") }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex">
|
<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
|
<ButtonSecondary
|
||||||
v-if="response.body"
|
v-if="response.body"
|
||||||
ref="downloadResponse"
|
ref="downloadResponse"
|
||||||
@@ -35,80 +43,97 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<div ref="xmlResponse"></div>
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "@nuxtjs/composition-api"
|
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
|
||||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import "codemirror/mode/xml/xml"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
mixins: [TextContentRendererMixin],
|
response: HoppRESTResponse
|
||||||
props: {
|
}>()
|
||||||
response: { type: Object, default: () => {} },
|
|
||||||
},
|
const {
|
||||||
data() {
|
$toast,
|
||||||
return {
|
app: { i18n },
|
||||||
copyIcon: "copy",
|
} = useContext()
|
||||||
downloadIcon: "download",
|
const t = i18n.t.bind(i18n)
|
||||||
}
|
|
||||||
},
|
const responseBodyText = computed(() => {
|
||||||
computed: {
|
if (
|
||||||
responseType() {
|
props.response.type === "loading" ||
|
||||||
return (
|
props.response.type === "network_fail"
|
||||||
this.response.headers.find(
|
)
|
||||||
(h) => h.key.toLowerCase() === "content-type"
|
return ""
|
||||||
).value || ""
|
if (typeof props.response.body === "string") return props.response.body
|
||||||
)
|
else {
|
||||||
.split(";")[0]
|
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||||
.toLowerCase()
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
},
|
return res.replace(/\0+$/, "")
|
||||||
},
|
}
|
||||||
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 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>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -150,6 +150,16 @@ export default defineComponent({
|
|||||||
|
|
||||||
handleKeystroke(event) {
|
handleKeystroke(event) {
|
||||||
switch (event.code) {
|
switch (event.code) {
|
||||||
|
case "Enter":
|
||||||
|
event.preventDefault()
|
||||||
|
if (this.currentSuggestionIndex > -1)
|
||||||
|
this.forceSuggestion(
|
||||||
|
this.suggestions.find(
|
||||||
|
(_item, index) => index === this.currentSuggestionIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.currentSuggestionIndex =
|
this.currentSuggestionIndex =
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ export default defineComponent({
|
|||||||
line-height: 1.9;
|
line-height: 1.9;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@apply text-secondaryDark;
|
@apply text-secondary;
|
||||||
@apply opacity-25;
|
@apply opacity-25;
|
||||||
@apply pointer-events-none;
|
@apply pointer-events-none;
|
||||||
|
|
||||||
@@ -501,7 +501,6 @@ export default defineComponent({
|
|||||||
@apply overflow-y-hidden;
|
@apply overflow-y-hidden;
|
||||||
@apply resize-none;
|
@apply resize-none;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
@apply transition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.env-input::-webkit-scrollbar {
|
.env-input::-webkit-scrollbar {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -150,8 +150,6 @@ function getCodegenGeneralRESTInfo(
|
|||||||
.map((x) => ({ ...x, active: true }))
|
.map((x) => ({ ...x, active: true }))
|
||||||
: request.effectiveFinalHeaders.map((x) => ({ ...x, active: true }))
|
: request.effectiveFinalHeaders.map((x) => ({ ...x, active: true }))
|
||||||
|
|
||||||
console.log(finalHeaders)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: request.name,
|
name: request.name,
|
||||||
uri: request.effectiveFinalURL,
|
uri: request.effectiveFinalURL,
|
||||||
|
|||||||
215
helpers/editor/codemirror.ts
Normal file
215
helpers/editor/codemirror.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
27
helpers/editor/completion/gqlQuery.ts
Normal file
27
helpers/editor/completion/gqlQuery.ts
Normal 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
|
||||||
23
helpers/editor/completion/index.ts
Normal file
23
helpers/editor/completion/index.ts
Normal 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>
|
||||||
24
helpers/editor/completion/preRequest.ts
Normal file
24
helpers/editor/completion/preRequest.ts
Normal 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
|
||||||
24
helpers/editor/completion/testScript.ts
Normal file
24
helpers/editor/completion/testScript.ts
Normal 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
|
||||||
58
helpers/editor/linting/gqlQuery.ts
Normal file
58
helpers/editor/linting/gqlQuery.ts
Normal 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",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
21
helpers/editor/linting/json.ts
Normal file
21
helpers/editor/linting/json.ts
Normal 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
|
||||||
7
helpers/editor/linting/linter.ts
Normal file
7
helpers/editor/linting/linter.ts
Normal 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[]>
|
||||||
69
helpers/editor/linting/preRequest.ts
Normal file
69
helpers/editor/linting/preRequest.ts
Normal 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
|
||||||
69
helpers/editor/linting/testScript.ts
Normal file
69
helpers/editor/linting/testScript.ts
Normal 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
|
||||||
80
helpers/editor/modes/graphql.ts
Normal file
80
helpers/editor/modes/graphql.ts
Normal 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)
|
||||||
|
}
|
||||||
38
helpers/editor/utils.ts
Normal file
38
helpers/editor/utils.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
const mimeToMode = {
|
const mimeToMode = {
|
||||||
"text/plain": "plain_text",
|
"text/plain": "text/x-yaml",
|
||||||
"text/html": "html",
|
"text/html": "htmlmixed",
|
||||||
"application/xml": "xml",
|
"application/xml": "application/xml",
|
||||||
"application/hal+json": "json",
|
"application/hal+json": "application/ld+json",
|
||||||
"application/vnd.api+json": "json",
|
"application/vnd.api+json": "application/ld+json",
|
||||||
"application/json": "json",
|
"application/json": "application/ld+json",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEditorLangForMimeType(mimeType) {
|
export function getEditorLangForMimeType(mimeType) {
|
||||||
return mimeToMode[mimeType] || "plain_text"
|
return mimeToMode[mimeType] || "text/x-yaml"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,75 @@
|
|||||||
* - end: int - the end exclusive offset of the syntax error
|
* - 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
|
string = str
|
||||||
strLen = str.length
|
strLen = str.length
|
||||||
start = end = lastEnd = -1
|
start = end = lastEnd = -1
|
||||||
@@ -37,15 +105,15 @@ export default function jsonParse(str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let string
|
let string: string
|
||||||
let strLen
|
let strLen: number
|
||||||
let start
|
let start: number
|
||||||
let end
|
let end: number
|
||||||
let lastEnd
|
let lastEnd: number
|
||||||
let code
|
let code: number
|
||||||
let kind
|
let kind: string
|
||||||
|
|
||||||
function parseObj() {
|
function parseObj(): JSONObjectValue {
|
||||||
const nodeStart = start
|
const nodeStart = start
|
||||||
const members = []
|
const members = []
|
||||||
expect("{")
|
expect("{")
|
||||||
@@ -63,9 +131,9 @@ function parseObj() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMember() {
|
function parseMember(): JSONObjectMember {
|
||||||
const nodeStart = start
|
const nodeStart = start
|
||||||
const key = kind === "String" ? curToken() : null
|
const key = kind === "String" ? (curToken() as JSONStringValue) : null
|
||||||
expect("String")
|
expect("String")
|
||||||
expect(":")
|
expect(":")
|
||||||
const value = parseVal()
|
const value = parseVal()
|
||||||
@@ -73,14 +141,14 @@ function parseMember() {
|
|||||||
kind: "Member",
|
kind: "Member",
|
||||||
start: nodeStart,
|
start: nodeStart,
|
||||||
end: lastEnd,
|
end: lastEnd,
|
||||||
key,
|
key: key!,
|
||||||
value,
|
value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArr() {
|
function parseArr(): JSONArrayValue {
|
||||||
const nodeStart = start
|
const nodeStart = start
|
||||||
const values = []
|
const values: JSONValue[] = []
|
||||||
expect("[")
|
expect("[")
|
||||||
if (!skip("]")) {
|
if (!skip("]")) {
|
||||||
do {
|
do {
|
||||||
@@ -96,7 +164,7 @@ function parseArr() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseVal() {
|
function parseVal(): JSONValue {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "[":
|
case "[":
|
||||||
return parseArr()
|
return parseArr()
|
||||||
@@ -111,14 +179,19 @@ function parseVal() {
|
|||||||
lex()
|
lex()
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
return expect("Value")
|
return expect("Value") as never
|
||||||
}
|
}
|
||||||
|
|
||||||
function curToken() {
|
function curToken(): JSONPrimitiveValue {
|
||||||
return { kind, start, end, value: JSON.parse(string.slice(start, end)) }
|
return {
|
||||||
|
kind: kind as any,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
value: JSON.parse(string.slice(start, end)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function expect(str) {
|
function expect(str: string) {
|
||||||
if (kind === str) {
|
if (kind === str) {
|
||||||
lex()
|
lex()
|
||||||
return
|
return
|
||||||
@@ -137,11 +210,17 @@ function expect(str) {
|
|||||||
throw syntaxError(`Expected ${str} but found ${found}.`)
|
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 }
|
return { message, start, end }
|
||||||
}
|
}
|
||||||
|
|
||||||
function skip(k) {
|
function skip(k: string) {
|
||||||
if (kind === k) {
|
if (kind === k) {
|
||||||
lex()
|
lex()
|
||||||
return true
|
return true
|
||||||
@@ -227,7 +306,7 @@ function lex() {
|
|||||||
function readString() {
|
function readString() {
|
||||||
ch()
|
ch()
|
||||||
while (code !== 34 && code > 31) {
|
while (code !== 34 && code > 31) {
|
||||||
if (code === 92) {
|
if (code === (92 as any)) {
|
||||||
// \
|
// \
|
||||||
ch()
|
ch()
|
||||||
switch (code) {
|
switch (code) {
|
||||||
@@ -299,7 +378,7 @@ function readNumber() {
|
|||||||
if (code === 69 || code === 101) {
|
if (code === 69 || code === 101) {
|
||||||
// E e
|
// E e
|
||||||
ch()
|
ch()
|
||||||
if (code === 43 || code === 45) {
|
if (code === (43 as any) || code === (45 as any)) {
|
||||||
// + -
|
// + -
|
||||||
ch()
|
ch()
|
||||||
}
|
}
|
||||||
100
helpers/newOutline.ts
Normal file
100
helpers/newOutline.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
>
|
>
|
||||||
<Pane class="flex flex-1 hide-scrollbar !overflow-auto">
|
<Pane class="flex flex-1 hide-scrollbar !overflow-auto">
|
||||||
<main class="flex flex-1 w-full">
|
<main class="flex flex-1 w-full">
|
||||||
<nuxt class="flex flex-1" />
|
<nuxt class="flex overflow-y-auto flex-1" />
|
||||||
</main>
|
</main>
|
||||||
</Pane>
|
</Pane>
|
||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
|
|||||||
@@ -421,6 +421,7 @@
|
|||||||
"file_imported": "File imported",
|
"file_imported": "File imported",
|
||||||
"finished_in": "Finished in {duration}ms",
|
"finished_in": "Finished in {duration}ms",
|
||||||
"history_deleted": "History deleted",
|
"history_deleted": "History deleted",
|
||||||
|
"linewrap": "Wrap lines",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"nothing_found": "Nothing found for",
|
"nothing_found": "Nothing found for",
|
||||||
|
|||||||
134
modules/emit-volar-types.ts
Normal file
134
modules/emit-volar-types.ts
Normal 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
|
||||||
@@ -133,6 +133,7 @@ export default {
|
|||||||
"@nuxtjs/composition-api/module",
|
"@nuxtjs/composition-api/module",
|
||||||
// https://github.com/antfu/unplugin-vue2-script-setup
|
// https://github.com/antfu/unplugin-vue2-script-setup
|
||||||
"unplugin-vue2-script-setup/nuxt",
|
"unplugin-vue2-script-setup/nuxt",
|
||||||
|
"~/modules/emit-volar-types.ts",
|
||||||
],
|
],
|
||||||
|
|
||||||
// Modules (https://go.nuxtjs.dev/config-modules)
|
// Modules (https://go.nuxtjs.dev/config-modules)
|
||||||
@@ -280,7 +281,7 @@ export default {
|
|||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
include: /(node_modules)/,
|
include: /(node_modules)/,
|
||||||
exclude: /(node_modules)\/(ace-builds)|(@firebase)/,
|
exclude: /(node_modules)\/(@firebase)/,
|
||||||
loader: "babel-loader",
|
loader: "babel-loader",
|
||||||
options: {
|
options: {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
102
package-lock.json
generated
102
package-lock.json
generated
@@ -15,15 +15,17 @@
|
|||||||
"@nuxtjs/robots": "^2.5.0",
|
"@nuxtjs/robots": "^2.5.0",
|
||||||
"@nuxtjs/sitemap": "^2.4.0",
|
"@nuxtjs/sitemap": "^2.4.0",
|
||||||
"@nuxtjs/toast": "^3.3.1",
|
"@nuxtjs/toast": "^3.3.1",
|
||||||
"ace-builds": "^1.4.12",
|
|
||||||
"acorn": "^8.5.0",
|
"acorn": "^8.5.0",
|
||||||
"acorn-walk": "^8.2.0",
|
"acorn-walk": "^8.2.0",
|
||||||
|
"codemirror": "^5.62.3",
|
||||||
|
"codemirror-theme-github": "^1.0.0",
|
||||||
"core-js": "^3.17.3",
|
"core-js": "^3.17.3",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"firebase": "^9.0.2",
|
"firebase": "^9.0.2",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.4.6",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.0",
|
||||||
"graphql-language-service-interface": "^2.8.4",
|
"graphql-language-service-interface": "^2.8.4",
|
||||||
|
"graphql-language-service-parser": "^1.9.2",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
@@ -60,7 +62,9 @@
|
|||||||
"@nuxtjs/stylelint-module": "^4.0.0",
|
"@nuxtjs/stylelint-module": "^4.0.0",
|
||||||
"@nuxtjs/svg": "^0.2.0",
|
"@nuxtjs/svg": "^0.2.0",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
|
"@types/codemirror": "^5.60.2",
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
|
"@types/esprima": "^4.0.3",
|
||||||
"@types/lodash": "^4.14.172",
|
"@types/lodash": "^4.14.172",
|
||||||
"@types/splitpanes": "^2.2.1",
|
"@types/splitpanes": "^2.2.1",
|
||||||
"@vue/runtime-dom": "^3.2.11",
|
"@vue/runtime-dom": "^3.2.11",
|
||||||
@@ -7915,6 +7919,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/codemirror": {
|
||||||
|
"version": "5.60.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.2.tgz",
|
||||||
|
"integrity": "sha512-tk8YxckrdU49GaJYRKxdzzzXrTlyT2nQGnobb8rAk34jt+kYXOxPKGqNgr7SJpl5r6YGaRD4CDfqiL+6A+/z7w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/tern": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/component-emitter": {
|
"node_modules/@types/component-emitter": {
|
||||||
"version": "1.2.10",
|
"version": "1.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
||||||
@@ -7964,6 +7977,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
|
||||||
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
|
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/esprima": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/esprima/-/esprima-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-jo14dIWVVtF0iMsKkYek6++4cWJjwpvog+rchLulwgFJGTXqIeTdCOvY0B3yMLTaIwMcKCdJ6mQbSR6wYHy98A==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/estree": {
|
||||||
|
"version": "0.0.50",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
|
||||||
|
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/etag": {
|
"node_modules/@types/etag": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.0.tgz",
|
||||||
@@ -8348,6 +8376,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz",
|
||||||
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ=="
|
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/tern": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/terser-webpack-plugin": {
|
"node_modules/@types/terser-webpack-plugin": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-4.2.1.tgz",
|
||||||
@@ -9406,11 +9443,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ace-builds": {
|
|
||||||
"version": "1.4.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.12.tgz",
|
|
||||||
"integrity": "sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg=="
|
|
||||||
},
|
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.5.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
|
||||||
@@ -13144,6 +13176,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codemirror": {
|
||||||
|
"version": "5.62.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz",
|
||||||
|
"integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg=="
|
||||||
|
},
|
||||||
|
"node_modules/codemirror-theme-github": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror-theme-github/-/codemirror-theme-github-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-suheFec2wlI4klyqn61MOFXjjrKPZiNY7d2py0OvTd5Z+7AsNxoGKDaS/HI59y7EAG1SkkXW/JQ1Rt2gDMxHfA=="
|
||||||
|
},
|
||||||
"node_modules/collect-v8-coverage": {
|
"node_modules/collect-v8-coverage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||||
@@ -42492,6 +42534,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/codemirror": {
|
||||||
|
"version": "5.60.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.2.tgz",
|
||||||
|
"integrity": "sha512-tk8YxckrdU49GaJYRKxdzzzXrTlyT2nQGnobb8rAk34jt+kYXOxPKGqNgr7SJpl5r6YGaRD4CDfqiL+6A+/z7w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/tern": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/component-emitter": {
|
"@types/component-emitter": {
|
||||||
"version": "1.2.10",
|
"version": "1.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
||||||
@@ -42541,6 +42592,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
|
||||||
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
|
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
|
||||||
},
|
},
|
||||||
|
"@types/esprima": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/esprima/-/esprima-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-jo14dIWVVtF0iMsKkYek6++4cWJjwpvog+rchLulwgFJGTXqIeTdCOvY0B3yMLTaIwMcKCdJ6mQbSR6wYHy98A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/estree": {
|
||||||
|
"version": "0.0.50",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
|
||||||
|
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/etag": {
|
"@types/etag": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.0.tgz",
|
||||||
@@ -42925,6 +42991,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz",
|
||||||
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ=="
|
"integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ=="
|
||||||
},
|
},
|
||||||
|
"@types/tern": {
|
||||||
|
"version": "0.23.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.4.tgz",
|
||||||
|
"integrity": "sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/terser-webpack-plugin": {
|
"@types/terser-webpack-plugin": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-4.2.1.tgz",
|
||||||
@@ -43810,11 +43885,6 @@
|
|||||||
"negotiator": "0.6.2"
|
"negotiator": "0.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ace-builds": {
|
|
||||||
"version": "1.4.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.12.tgz",
|
|
||||||
"integrity": "sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg=="
|
|
||||||
},
|
|
||||||
"acorn": {
|
"acorn": {
|
||||||
"version": "8.5.0",
|
"version": "8.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
|
||||||
@@ -46771,6 +46841,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||||
},
|
},
|
||||||
|
"codemirror": {
|
||||||
|
"version": "5.62.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.3.tgz",
|
||||||
|
"integrity": "sha512-zZAyOfN8TU67ngqrxhOgtkSAGV9jSpN1snbl8elPtnh9Z5A11daR405+dhLzLnuXrwX0WCShWlybxPN3QC/9Pg=="
|
||||||
|
},
|
||||||
|
"codemirror-theme-github": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror-theme-github/-/codemirror-theme-github-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-suheFec2wlI4klyqn61MOFXjjrKPZiNY7d2py0OvTd5Z+7AsNxoGKDaS/HI59y7EAG1SkkXW/JQ1Rt2gDMxHfA=="
|
||||||
|
},
|
||||||
"collect-v8-coverage": {
|
"collect-v8-coverage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||||
|
|||||||
@@ -31,15 +31,17 @@
|
|||||||
"@nuxtjs/robots": "^2.5.0",
|
"@nuxtjs/robots": "^2.5.0",
|
||||||
"@nuxtjs/sitemap": "^2.4.0",
|
"@nuxtjs/sitemap": "^2.4.0",
|
||||||
"@nuxtjs/toast": "^3.3.1",
|
"@nuxtjs/toast": "^3.3.1",
|
||||||
"ace-builds": "^1.4.12",
|
|
||||||
"acorn": "^8.5.0",
|
"acorn": "^8.5.0",
|
||||||
"acorn-walk": "^8.2.0",
|
"acorn-walk": "^8.2.0",
|
||||||
|
"codemirror": "^5.62.3",
|
||||||
|
"codemirror-theme-github": "^1.0.0",
|
||||||
"core-js": "^3.17.3",
|
"core-js": "^3.17.3",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"firebase": "^9.0.2",
|
"firebase": "^9.0.2",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.4.6",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.0",
|
||||||
"graphql-language-service-interface": "^2.8.4",
|
"graphql-language-service-interface": "^2.8.4",
|
||||||
|
"graphql-language-service-parser": "^1.9.2",
|
||||||
"json-loader": "^0.5.7",
|
"json-loader": "^0.5.7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
@@ -76,7 +78,9 @@
|
|||||||
"@nuxtjs/stylelint-module": "^4.0.0",
|
"@nuxtjs/stylelint-module": "^4.0.0",
|
||||||
"@nuxtjs/svg": "^0.2.0",
|
"@nuxtjs/svg": "^0.2.0",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
|
"@types/codemirror": "^5.60.2",
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
|
"@types/esprima": "^4.0.3",
|
||||||
"@types/lodash": "^4.14.172",
|
"@types/lodash": "^4.14.172",
|
||||||
"@types/splitpanes": "^2.2.1",
|
"@types/splitpanes": "^2.2.1",
|
||||||
"@vue/runtime-dom": "^3.2.11",
|
"@vue/runtime-dom": "^3.2.11",
|
||||||
|
|||||||
@@ -61,17 +61,12 @@
|
|||||||
@click.native="collectionJSON = '[]'"
|
@click.native="collectionJSON = '[]'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SmartAceEditor
|
<textarea-autosize
|
||||||
|
id="import-curl"
|
||||||
v-model="collectionJSON"
|
v-model="collectionJSON"
|
||||||
:lang="'json'"
|
class="font-mono p-4 bg-primary"
|
||||||
:lint="false"
|
autofocus
|
||||||
:options="{
|
rows="8"
|
||||||
maxLines: Infinity,
|
|
||||||
minLines: 16,
|
|
||||||
autoScrollEditorIntoView: true,
|
|
||||||
showPrintMargin: false,
|
|
||||||
useWorker: false,
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="
|
class="
|
||||||
|
|||||||
@@ -1,36 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<Splitpanes
|
||||||
<Splitpanes
|
class="smart-splitter"
|
||||||
class="smart-splitter"
|
:dbl-click-splitter="false"
|
||||||
:dbl-click-splitter="false"
|
:horizontal="!(windowInnerWidth.x.value >= 768)"
|
||||||
: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">
|
<GraphqlSidebar :conn="gqlConn" />
|
||||||
<Splitpanes
|
</Pane>
|
||||||
class="smart-splitter"
|
</Splitpanes>
|
||||||
: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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -298,7 +298,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2 py-4 items-center">
|
<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
|
<input
|
||||||
id="url"
|
id="url"
|
||||||
v-model="PROXY_URL"
|
v-model="PROXY_URL"
|
||||||
|
|||||||
Reference in New Issue
Block a user