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
|
||||
|
||||
# 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 {
|
||||
@apply bg-divider bg-clip-content;
|
||||
@apply rounded-full;
|
||||
@apply border-solid border-4 border-transparent;
|
||||
@apply border-solid border-transparent border-4;
|
||||
@apply hover:(bg-dividerDark bg-clip-content);
|
||||
}
|
||||
|
||||
@@ -36,8 +36,9 @@
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
@apply text-secondaryDark;
|
||||
textarea::placeholder,
|
||||
.CodeMirror-empty {
|
||||
@apply text-secondary;
|
||||
@apply opacity-25;
|
||||
}
|
||||
|
||||
@@ -116,8 +117,8 @@ a {
|
||||
|
||||
&.link {
|
||||
@apply items-center;
|
||||
@apply px-1 py-0.5;
|
||||
@apply -mx-1 -my-0.5;
|
||||
@apply py-0.5 px-1;
|
||||
@apply -my-0.5 -mx-1;
|
||||
@apply text-accent;
|
||||
@apply rounded;
|
||||
@apply hover:text-accentDark;
|
||||
@@ -198,7 +199,7 @@ hr {
|
||||
.textarea {
|
||||
@apply flex;
|
||||
@apply w-full;
|
||||
@apply px-4 py-2;
|
||||
@apply py-2 px-4;
|
||||
@apply bg-transparent;
|
||||
@apply rounded;
|
||||
@apply text-secondaryDark;
|
||||
@@ -293,7 +294,7 @@ input[type="checkbox"] {
|
||||
@apply cursor-pointer;
|
||||
|
||||
&::before {
|
||||
@apply border-2 border-divider;
|
||||
@apply border-divider border-2;
|
||||
@apply rounded;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@@ -347,6 +348,7 @@ input[type="checkbox"] {
|
||||
@apply justify-start;
|
||||
@apply shadow;
|
||||
@apply font-medium;
|
||||
@apply transition;
|
||||
|
||||
font-size: var(--body-font-size);
|
||||
line-height: var(--body-line-height);
|
||||
@@ -358,7 +360,6 @@ input[type="checkbox"] {
|
||||
@apply ml-auto;
|
||||
@apply last:ml-4;
|
||||
@apply sm:ml-8;
|
||||
@apply transition;
|
||||
@apply rounded;
|
||||
@apply text-current;
|
||||
@apply normal-case;
|
||||
@@ -461,6 +462,32 @@ input[type="checkbox"] {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
@apply !h-auto;
|
||||
|
||||
font-size: var(--body-font-size);
|
||||
|
||||
&:not(.CodeMirror-focused) .CodeMirror-activeline-background {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog-top {
|
||||
@apply bg-primaryLight;
|
||||
@apply border-dividerLight;
|
||||
@apply px-4;
|
||||
@apply py-2;
|
||||
@apply z-5;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
@apply min-h-64;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
main {
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="mode === 'email'" class="flex flex-col space-y-2">
|
||||
<div class="flex items-center relative">
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
|
||||
@@ -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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "@nuxtjs/composition-api"
|
||||
<script setup lang="ts">
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { useReadonlyStream, useStream } from "~/helpers/utils/composables"
|
||||
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||
const props = defineProps<{
|
||||
conn: GQLConnection
|
||||
}>()
|
||||
|
||||
const url = useStream(gqlURL$, "", setGQLURL)
|
||||
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||
|
||||
const onConnectClick = () => {
|
||||
if (!connected.value) {
|
||||
props.conn.connect(url.value, headers.value as any)
|
||||
const url = useStream(gqlURL$, "", setGQLURL)
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-schema",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
} else {
|
||||
props.conn.disconnect()
|
||||
}
|
||||
}
|
||||
const onConnectClick = () => {
|
||||
if (!connected.value) {
|
||||
props.conn.connect(url.value, headers.value as any)
|
||||
|
||||
return {
|
||||
url,
|
||||
connected,
|
||||
onConnectClick,
|
||||
}
|
||||
},
|
||||
})
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-schema",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
} else {
|
||||
props.conn.disconnect()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -42,9 +42,7 @@
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${$t(
|
||||
'action.prettify'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>P</kbd>`"
|
||||
:title="$t('action.prettify')"
|
||||
:svg="prettifyQueryIcon"
|
||||
@click.native="prettifyQuery"
|
||||
/>
|
||||
@@ -57,20 +55,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<GraphqlQueryEditor
|
||||
ref="queryEditor"
|
||||
v-model="gqlQueryString"
|
||||
:on-run-g-q-l-query="runQuery"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
@update-query="updateQuery"
|
||||
/>
|
||||
<div ref="queryEditor"></div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
|
||||
@@ -108,19 +93,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
ref="variableEditor"
|
||||
v-model="variableString"
|
||||
:lang="'json'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
<div ref="variableEditor"></div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
|
||||
@@ -173,27 +146,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bulkMode" class="flex">
|
||||
<textarea-autosize
|
||||
v-model="bulkHeaders"
|
||||
v-focus
|
||||
name="bulk-parameters"
|
||||
class="
|
||||
bg-transparent
|
||||
border-b border-dividerLight
|
||||
flex
|
||||
font-mono
|
||||
flex-1
|
||||
py-2
|
||||
px-4
|
||||
whitespace-pre
|
||||
resize-y
|
||||
overflow-auto
|
||||
"
|
||||
rows="10"
|
||||
:placeholder="$t('state.bulk_mode_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="bulkMode" ref="bulkEditor"></div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(header, index) in headers"
|
||||
@@ -229,7 +182,9 @@
|
||||
/>
|
||||
<input
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('count.value', { count: index + 1 })"
|
||||
:placeholder="
|
||||
$t('count.value', { count: index + 1 }).toString()
|
||||
"
|
||||
:name="`value ${index}`"
|
||||
:value="header.value"
|
||||
autofocus
|
||||
@@ -311,17 +266,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
onMounted,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api"
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, useContext, watch } from "@nuxtjs/composition-api"
|
||||
import clone from "lodash/clone"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import * as gql from "graphql"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import {
|
||||
useNuxt,
|
||||
@@ -348,208 +296,207 @@ import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import "codemirror/mode/javascript/javascript"
|
||||
import "~/helpers/editor/modes/graphql"
|
||||
import jsonLinter from "~/helpers/editor/linting/json"
|
||||
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
|
||||
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
const nuxt = useNuxt()
|
||||
const props = defineProps<{
|
||||
conn: GQLConnection
|
||||
}>()
|
||||
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
const nuxt = useNuxt()
|
||||
|
||||
watch(bulkHeaders, () => {
|
||||
try {
|
||||
const transformation = bulkHeaders.value.split("\n").map((item) => ({
|
||||
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
||||
value: item.substring(item.indexOf(":") + 1).trim(),
|
||||
active: !item.trim().startsWith("//"),
|
||||
}))
|
||||
setGQLHeaders(transformation)
|
||||
} catch (e) {
|
||||
$toast.error(t("error.something_went_wrong").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
|
||||
watch(bulkHeaders, () => {
|
||||
try {
|
||||
const transformation = bulkHeaders.value.split("\n").map((item) => ({
|
||||
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
||||
value: item.substring(item.indexOf(":") + 1).trim(),
|
||||
active: !item.trim().startsWith("//"),
|
||||
}))
|
||||
setGQLHeaders(transformation)
|
||||
} catch (e) {
|
||||
$toast.error(t("error.something_went_wrong").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
const url = useReadonlyStream(gqlURL$, "")
|
||||
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
|
||||
const variableString = useStream(gqlVariables$, "", setGQLVariables)
|
||||
const headers = useStream(gqlHeaders$, [], setGQLHeaders)
|
||||
const url = useReadonlyStream(gqlURL$, "")
|
||||
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
|
||||
const variableString = useStream(gqlVariables$, "", setGQLVariables)
|
||||
const headers = useStream(gqlHeaders$, [], setGQLHeaders)
|
||||
|
||||
const queryEditor = ref<any | null>(null)
|
||||
const bulkEditor = ref<any | null>(null)
|
||||
|
||||
const copyQueryIcon = ref("copy")
|
||||
const prettifyQueryIcon = ref("align-left")
|
||||
const copyVariablesIcon = ref("copy")
|
||||
useCodemirror(bulkEditor, bulkHeaders, {
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: t("state.bulk_mode_placeholder").toString(),
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
|
||||
const showSaveRequestModal = ref(false)
|
||||
const variableEditor = ref<any | null>(null)
|
||||
|
||||
const schema = useReadonlyStream(props.conn.schemaString$, "")
|
||||
useCodemirror(variableEditor, variableString, {
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
placeholder: t("request.variables").toString(),
|
||||
},
|
||||
linter: jsonLinter,
|
||||
completer: null,
|
||||
})
|
||||
|
||||
watch(
|
||||
headers,
|
||||
() => {
|
||||
if (
|
||||
(headers.value[headers.value.length - 1]?.key !== "" ||
|
||||
headers.value[headers.value.length - 1]?.value !== "") &&
|
||||
headers.value.length
|
||||
)
|
||||
addRequestHeader()
|
||||
},
|
||||
{ deep: true }
|
||||
const queryEditor = ref<any | null>(null)
|
||||
const schemaString = useReadonlyStream(props.conn.schema$, null)
|
||||
|
||||
useCodemirror(queryEditor, gqlQueryString, {
|
||||
extendedEditorConfig: {
|
||||
mode: "graphql",
|
||||
placeholder: t("request.query").toString(),
|
||||
},
|
||||
linter: createGQLQueryLinter(schemaString),
|
||||
completer: queryCompleter(schemaString),
|
||||
})
|
||||
|
||||
const copyQueryIcon = ref("copy")
|
||||
const prettifyQueryIcon = ref("align-left")
|
||||
const copyVariablesIcon = ref("copy")
|
||||
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
watch(
|
||||
headers,
|
||||
() => {
|
||||
if (
|
||||
(headers.value[headers.value.length - 1]?.key !== "" ||
|
||||
headers.value[headers.value.length - 1]?.value !== "") &&
|
||||
headers.value.length
|
||||
)
|
||||
addRequestHeader()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!headers.value?.length) {
|
||||
addRequestHeader()
|
||||
}
|
||||
})
|
||||
|
||||
const copyQuery = () => {
|
||||
copyToClipboard(gqlQueryString.value)
|
||||
copyQueryIcon.value = "check"
|
||||
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const response = useStream(gqlResponse$, "", setGQLResponse)
|
||||
|
||||
const runQuery = async () => {
|
||||
const startTime = Date.now()
|
||||
|
||||
nuxt.value.$loading.start()
|
||||
response.value = t("state.loading").toString()
|
||||
|
||||
try {
|
||||
const runURL = clone(url.value)
|
||||
const runHeaders = clone(headers.value)
|
||||
const runQuery = clone(gqlQueryString.value)
|
||||
const runVariables = clone(variableString.value)
|
||||
|
||||
const responseText = await props.conn.runQuery(
|
||||
runURL,
|
||||
runHeaders,
|
||||
runQuery,
|
||||
runVariables
|
||||
)
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
nuxt.value.$loading.finish()
|
||||
|
||||
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
|
||||
|
||||
addGraphqlHistoryEntry(
|
||||
makeGQLHistoryEntry({
|
||||
request: makeGQLRequest({
|
||||
name: "",
|
||||
url: runURL,
|
||||
query: runQuery,
|
||||
headers: runHeaders,
|
||||
variables: runVariables,
|
||||
}),
|
||||
response: response.value,
|
||||
star: false,
|
||||
})
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!headers.value?.length) {
|
||||
addRequestHeader()
|
||||
}
|
||||
$toast.success(t("state.finished_in", { duration }).toString(), {
|
||||
icon: "done",
|
||||
})
|
||||
} catch (e: any) {
|
||||
response.value = `${e}. ${t("error.check_console_details")}`
|
||||
nuxt.value.$loading.finish()
|
||||
|
||||
const copyQuery = () => {
|
||||
copyToClipboard(gqlQueryString.value)
|
||||
copyQueryIcon.value = "check"
|
||||
setTimeout(() => (copyQueryIcon.value = "copy"), 1000)
|
||||
}
|
||||
$toast.error(`${e} ${t("error.f12_details").toString()}`, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
const response = useStream(gqlResponse$, "", setGQLResponse)
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-query",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
}
|
||||
|
||||
const runQuery = async () => {
|
||||
const startTime = Date.now()
|
||||
const hideRequestModal = () => {
|
||||
showSaveRequestModal.value = false
|
||||
}
|
||||
|
||||
nuxt.value.$loading.start()
|
||||
response.value = t("state.loading").toString()
|
||||
const prettifyQuery = () => {
|
||||
try {
|
||||
gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
|
||||
} catch (e) {
|
||||
$toast.error(t("error.gql_prettify_invalid_query").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
prettifyQueryIcon.value = "check"
|
||||
setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000)
|
||||
}
|
||||
|
||||
try {
|
||||
const runURL = clone(url.value)
|
||||
const runHeaders = clone(headers.value)
|
||||
const runQuery = clone(gqlQueryString.value)
|
||||
const runVariables = clone(variableString.value)
|
||||
const saveRequest = () => {
|
||||
showSaveRequestModal.value = true
|
||||
}
|
||||
|
||||
const responseText = await props.conn.runQuery(
|
||||
runURL,
|
||||
runHeaders,
|
||||
runQuery,
|
||||
runVariables
|
||||
)
|
||||
const duration = Date.now() - startTime
|
||||
const copyVariables = () => {
|
||||
copyToClipboard(variableString.value)
|
||||
copyVariablesIcon.value = "check"
|
||||
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
nuxt.value.$loading.finish()
|
||||
const addRequestHeader = () => {
|
||||
addGQLHeader({
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
|
||||
|
||||
addGraphqlHistoryEntry(
|
||||
makeGQLHistoryEntry({
|
||||
request: makeGQLRequest({
|
||||
name: "",
|
||||
url: runURL,
|
||||
query: runQuery,
|
||||
headers: runHeaders,
|
||||
variables: runVariables,
|
||||
}),
|
||||
response: response.value,
|
||||
star: false,
|
||||
})
|
||||
)
|
||||
|
||||
$toast.success(t("state.finished_in", { duration }).toString(), {
|
||||
icon: "done",
|
||||
})
|
||||
} catch (e: any) {
|
||||
response.value = `${e}. ${t("error.check_console_details")}`
|
||||
nuxt.value.$loading.finish()
|
||||
|
||||
$toast.error(`${e} ${t("error.f12_details").toString()}`, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-query",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
}
|
||||
|
||||
const hideRequestModal = () => {
|
||||
showSaveRequestModal.value = false
|
||||
}
|
||||
|
||||
const prettifyQuery = () => {
|
||||
queryEditor.value.prettifyQuery()
|
||||
prettifyQueryIcon.value = "check"
|
||||
setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000)
|
||||
}
|
||||
|
||||
const saveRequest = () => {
|
||||
showSaveRequestModal.value = true
|
||||
}
|
||||
|
||||
// Why ?
|
||||
const updateQuery = (updatedQuery: string) => {
|
||||
gqlQueryString.value = updatedQuery
|
||||
}
|
||||
|
||||
const copyVariables = () => {
|
||||
copyToClipboard(variableString.value)
|
||||
copyVariablesIcon.value = "check"
|
||||
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const addRequestHeader = () => {
|
||||
addGQLHeader({
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
const removeRequestHeader = (index: number) => {
|
||||
removeGQLHeader(index)
|
||||
}
|
||||
|
||||
return {
|
||||
gqlQueryString,
|
||||
variableString,
|
||||
headers,
|
||||
copyQueryIcon,
|
||||
prettifyQueryIcon,
|
||||
copyVariablesIcon,
|
||||
|
||||
queryEditor,
|
||||
|
||||
showSaveRequestModal,
|
||||
hideRequestModal,
|
||||
|
||||
schema,
|
||||
|
||||
copyQuery,
|
||||
runQuery,
|
||||
prettifyQuery,
|
||||
saveRequest,
|
||||
updateQuery,
|
||||
copyVariables,
|
||||
addRequestHeader,
|
||||
removeRequestHeader,
|
||||
|
||||
getSpecialKey: getPlatformSpecialKey,
|
||||
|
||||
commonHeaders,
|
||||
updateGQLHeader,
|
||||
bulkMode,
|
||||
bulkHeaders,
|
||||
}
|
||||
},
|
||||
})
|
||||
const removeRequestHeader = (index: number) => {
|
||||
removeGQLHeader(index)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,6 +18,13 @@
|
||||
{{ $t("response.title") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="corner-down-left"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="downloadResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -34,21 +41,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
v-if="responseString"
|
||||
:value="responseString"
|
||||
:lang="'json'"
|
||||
:lint="false"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
<div v-if="responseString" ref="schemaEditor"></div>
|
||||
<div
|
||||
v-else
|
||||
class="
|
||||
@@ -60,35 +53,21 @@
|
||||
"
|
||||
>
|
||||
<div class="flex space-x-2 pb-4">
|
||||
<div class="flex flex-col space-y-4 items-end">
|
||||
<div class="flex flex-col space-y-4 text-right items-end">
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.request.send_request") }}
|
||||
</span>
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.show_all") }}
|
||||
</span>
|
||||
<!-- <span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.command_menu") }}
|
||||
</span>
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.help_menu") }}
|
||||
</span> -->
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
||||
<span class="shortcut-key">G</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
||||
<span class="shortcut-key">K</span>
|
||||
</div>
|
||||
<!-- <div class="flex">
|
||||
<span class="shortcut-key">/</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">?</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonSecondary
|
||||
@@ -103,77 +82,66 @@
|
||||
</AppSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import { gqlResponse$ } from "~/newstore/GQLSession"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const responseString = useReadonlyStream(gqlResponse$, "")
|
||||
|
||||
const schemaEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
schemaEditor,
|
||||
responseString,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
)
|
||||
|
||||
const responseString = useReadonlyStream(gqlResponse$, "")
|
||||
const downloadResponseIcon = ref("download")
|
||||
const copyResponseIcon = ref("copy")
|
||||
|
||||
const downloadResponseIcon = ref("download")
|
||||
const copyResponseIcon = ref("copy")
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseString.value!)
|
||||
copyResponseIcon.value = "check"
|
||||
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseString.value!)
|
||||
copyResponseIcon.value = "check"
|
||||
setTimeout(() => (copyResponseIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseString.value
|
||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadResponseIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadResponseIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return {
|
||||
responseString,
|
||||
|
||||
downloadResponseIcon,
|
||||
copyResponseIcon,
|
||||
|
||||
downloadResponse,
|
||||
copyResponse,
|
||||
|
||||
getSpecialKey: getPlatformSpecialKey,
|
||||
}
|
||||
},
|
||||
})
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseString.value
|
||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadResponseIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadResponseIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -149,6 +149,13 @@
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="corner-down-left"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="downloadSchema"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -165,20 +172,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
v-if="schemaString"
|
||||
v-model="schemaString"
|
||||
:lang="'graphqlschema'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
<div v-if="schemaString" ref="schemaEditor"></div>
|
||||
<div
|
||||
v-else
|
||||
class="
|
||||
@@ -200,17 +194,17 @@
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
PropType,
|
||||
reactive,
|
||||
ref,
|
||||
useContext,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import { GraphQLField, GraphQLType } from "graphql"
|
||||
import { map } from "rxjs/operators"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
@@ -222,6 +216,7 @@ import {
|
||||
setGQLURL,
|
||||
setGQLVariables,
|
||||
} from "~/newstore/GQLSession"
|
||||
import "~/helpers/editor/modes/graphql"
|
||||
|
||||
function isTextFoundInGraphqlFieldObject(
|
||||
text: string,
|
||||
@@ -285,186 +280,168 @@ type GQLHistoryEntry = {
|
||||
variables: string
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
conn: {
|
||||
type: Object as PropType<GQLConnection>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
const props = defineProps<{
|
||||
conn: GQLConnection
|
||||
}>()
|
||||
|
||||
const queryFields = useReadonlyStream(
|
||||
props.conn.queryFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const mutationFields = useReadonlyStream(
|
||||
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const subscriptionFields = useReadonlyStream(
|
||||
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const graphqlTypes = useReadonlyStream(
|
||||
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const downloadSchemaIcon = ref("download")
|
||||
const copySchemaIcon = ref("copy")
|
||||
const queryFields = useReadonlyStream(
|
||||
props.conn.queryFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const graphqlFieldsFilterText = ref("")
|
||||
const mutationFields = useReadonlyStream(
|
||||
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const gqlTabs = ref<any | null>(null)
|
||||
const typesTab = ref<any | null>(null)
|
||||
const subscriptionFields = useReadonlyStream(
|
||||
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const filteredQueryFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
queryFields.value as any
|
||||
)
|
||||
})
|
||||
const graphqlTypes = useReadonlyStream(
|
||||
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
|
||||
[]
|
||||
)
|
||||
|
||||
const filteredMutationFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
mutationFields.value as any
|
||||
)
|
||||
})
|
||||
const downloadSchemaIcon = ref("download")
|
||||
const copySchemaIcon = ref("copy")
|
||||
|
||||
const filteredSubscriptionFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
subscriptionFields.value as any
|
||||
)
|
||||
})
|
||||
const graphqlFieldsFilterText = ref("")
|
||||
|
||||
const filteredGraphqlTypes = computed(() => {
|
||||
return getFilteredGraphqlTypes(
|
||||
graphqlFieldsFilterText.value,
|
||||
graphqlTypes.value as any
|
||||
)
|
||||
})
|
||||
const gqlTabs = ref<any | null>(null)
|
||||
const typesTab = ref<any | null>(null)
|
||||
|
||||
const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
|
||||
if (!graphqlFieldsFilterText.value) return false
|
||||
|
||||
return isTextFoundInGraphqlFieldObject(
|
||||
graphqlFieldsFilterText.value,
|
||||
gqlType as any
|
||||
)
|
||||
}
|
||||
|
||||
const getGqlTypeHighlightedFields = (gqlType: GraphQLType) => {
|
||||
if (!graphqlFieldsFilterText.value) return []
|
||||
|
||||
const fields = Object.values((gqlType as any)._fields || {})
|
||||
if (!fields || fields.length === 0) return []
|
||||
|
||||
return fields.filter((field) =>
|
||||
isTextFoundInGraphqlFieldObject(
|
||||
graphqlFieldsFilterText.value,
|
||||
field as any
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const handleJumpToType = async (type: GraphQLType) => {
|
||||
gqlTabs.value.selectTab(typesTab.value)
|
||||
await nextTick()
|
||||
|
||||
const rootTypeName = resolveRootType(type).name
|
||||
|
||||
const target = document.getElementById(`type_${rootTypeName}`)
|
||||
if (target) {
|
||||
gqlTabs.value.$el
|
||||
.querySelector(".gqlTabs")
|
||||
.scrollTo({ top: target.offsetTop, behavior: "smooth" })
|
||||
}
|
||||
}
|
||||
const schemaString = useReadonlyStream(
|
||||
props.conn.schemaString$.pipe(map((x) => x ?? "")),
|
||||
""
|
||||
)
|
||||
|
||||
const downloadSchema = () => {
|
||||
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
|
||||
const file = new Blob([dataToWrite], { type: "application/graphql" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${
|
||||
url.split("/").pop()!.split("#")[0].split("?")[0]
|
||||
}.graphql`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadSchemaIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadSchemaIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const copySchema = () => {
|
||||
if (!schemaString.value) return
|
||||
|
||||
copyToClipboard(schemaString.value)
|
||||
copySchemaIcon.value = "check"
|
||||
setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const handleUseHistory = (entry: GQLHistoryEntry) => {
|
||||
const url = entry.url
|
||||
const headers = entry.headers
|
||||
const gqlQueryString = entry.query
|
||||
const variableString = entry.variables
|
||||
const responseText = entry.response
|
||||
|
||||
setGQLURL(url)
|
||||
setGQLHeaders(headers)
|
||||
setGQLQuery(gqlQueryString)
|
||||
setGQLVariables(variableString)
|
||||
setGQLResponse(responseText)
|
||||
props.conn.reset()
|
||||
}
|
||||
|
||||
return {
|
||||
queryFields,
|
||||
mutationFields,
|
||||
subscriptionFields,
|
||||
graphqlTypes,
|
||||
schemaString,
|
||||
|
||||
graphqlFieldsFilterText,
|
||||
|
||||
filteredQueryFields,
|
||||
filteredMutationFields,
|
||||
filteredSubscriptionFields,
|
||||
filteredGraphqlTypes,
|
||||
|
||||
isGqlTypeHighlighted,
|
||||
getGqlTypeHighlightedFields,
|
||||
|
||||
gqlTabs,
|
||||
typesTab,
|
||||
handleJumpToType,
|
||||
|
||||
downloadSchema,
|
||||
downloadSchemaIcon,
|
||||
copySchemaIcon,
|
||||
copySchema,
|
||||
handleUseHistory,
|
||||
}
|
||||
},
|
||||
const filteredQueryFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
queryFields.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const filteredMutationFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
mutationFields.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const filteredSubscriptionFields = computed(() => {
|
||||
return getFilteredGraphqlFields(
|
||||
graphqlFieldsFilterText.value,
|
||||
subscriptionFields.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const filteredGraphqlTypes = computed(() => {
|
||||
return getFilteredGraphqlTypes(
|
||||
graphqlFieldsFilterText.value,
|
||||
graphqlTypes.value as any
|
||||
)
|
||||
})
|
||||
|
||||
const isGqlTypeHighlighted = (gqlType: GraphQLType) => {
|
||||
if (!graphqlFieldsFilterText.value) return false
|
||||
|
||||
return isTextFoundInGraphqlFieldObject(
|
||||
graphqlFieldsFilterText.value,
|
||||
gqlType as any
|
||||
)
|
||||
}
|
||||
|
||||
const getGqlTypeHighlightedFields = (gqlType: GraphQLType) => {
|
||||
if (!graphqlFieldsFilterText.value) return []
|
||||
|
||||
const fields = Object.values((gqlType as any)._fields || {})
|
||||
if (!fields || fields.length === 0) return []
|
||||
|
||||
return fields.filter((field) =>
|
||||
isTextFoundInGraphqlFieldObject(graphqlFieldsFilterText.value, field as any)
|
||||
)
|
||||
}
|
||||
|
||||
const handleJumpToType = async (type: GraphQLType) => {
|
||||
gqlTabs.value.selectTab(typesTab.value)
|
||||
await nextTick()
|
||||
|
||||
const rootTypeName = resolveRootType(type).name
|
||||
|
||||
const target = document.getElementById(`type_${rootTypeName}`)
|
||||
if (target) {
|
||||
gqlTabs.value.$el
|
||||
.querySelector(".gqlTabs")
|
||||
.scrollTo({ top: target.offsetTop, behavior: "smooth" })
|
||||
}
|
||||
}
|
||||
|
||||
const schemaString = useReadonlyStream(
|
||||
props.conn.schemaString$.pipe(map((x) => x ?? "")),
|
||||
""
|
||||
)
|
||||
|
||||
const schemaEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
schemaEditor,
|
||||
schemaString,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "graphql",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
)
|
||||
|
||||
const downloadSchema = () => {
|
||||
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
|
||||
const file = new Blob([dataToWrite], { type: "application/graphql" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.graphql`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadSchemaIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadSchemaIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const copySchema = () => {
|
||||
if (!schemaString.value) return
|
||||
|
||||
copyToClipboard(schemaString.value)
|
||||
copySchemaIcon.value = "check"
|
||||
setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const handleUseHistory = (entry: GQLHistoryEntry) => {
|
||||
const url = entry.url
|
||||
const headers = entry.headers
|
||||
const gqlQueryString = entry.query
|
||||
const variableString = entry.variables
|
||||
const responseText = entry.response
|
||||
|
||||
setGQLURL(url)
|
||||
setGQLHeaders(headers)
|
||||
setGQLQuery(gqlQueryString)
|
||||
setGQLVariables(variableString)
|
||||
setGQLResponse(responseText)
|
||||
props.conn.reset()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -38,31 +38,25 @@
|
||||
{{ t("request.generated_code") }}
|
||||
</label>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
<div
|
||||
v-if="codegenType"
|
||||
ref="generatedCode"
|
||||
:value="requestCode"
|
||||
:lang="codegens.find((x) => x.id === codegenType).language"
|
||||
:options="{
|
||||
maxLines: 16,
|
||||
minLines: 8,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border rounded border-dividerLight"
|
||||
/>
|
||||
class="border border-dividerLight rounded"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<ButtonPrimary
|
||||
ref="copyRequestCode"
|
||||
:label="t('action.copy')"
|
||||
:svg="copyIcon"
|
||||
@click.native="copyRequestCode"
|
||||
/>
|
||||
<ButtonSecondary :label="t('action.dismiss')" @click.native="hideModal" />
|
||||
<span class="flex">
|
||||
<ButtonPrimary
|
||||
:label="t('action.copy').toString()"
|
||||
:svg="copyIcon"
|
||||
@click.native="copyRequestCode"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="t('action.dismiss').toString()"
|
||||
@click.native="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</SmartModal>
|
||||
</template>
|
||||
@@ -70,6 +64,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useContext, watch } from "@nuxtjs/composition-api"
|
||||
import { codegens, generateCodegenContext } from "~/helpers/codegen/codegen"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { getEffectiveRESTRequest } from "~/helpers/utils/EffectiveURL"
|
||||
import { getCurrentEnvironment } from "~/newstore/environments"
|
||||
@@ -106,6 +101,17 @@ const requestCode = computed(() => {
|
||||
.generator(generateCodegenContext(effectiveRequest))
|
||||
})
|
||||
|
||||
const generatedCode = ref<any | null>(null)
|
||||
|
||||
useCodemirror(generatedCode, requestCode, {
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
readOnly: true,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(goingToShow) => {
|
||||
|
||||
@@ -47,27 +47,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bulkMode" class="flex">
|
||||
<textarea-autosize
|
||||
v-model="bulkHeaders"
|
||||
v-focus
|
||||
name="bulk-headers"
|
||||
class="
|
||||
bg-transparent
|
||||
border-b border-dividerLight
|
||||
flex
|
||||
font-mono
|
||||
flex-1
|
||||
py-2
|
||||
px-4
|
||||
whitespace-pre
|
||||
resize-y
|
||||
overflow-auto
|
||||
"
|
||||
rows="10"
|
||||
:placeholder="$t('state.bulk_mode_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="bulkMode" ref="bulkEditor"></div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(header, index) in headers$"
|
||||
@@ -193,96 +173,86 @@
|
||||
</AppSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ref, useContext, watch } from "@nuxtjs/composition-api"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
useContext,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import {
|
||||
restHeaders$,
|
||||
addRESTHeader,
|
||||
updateRESTHeader,
|
||||
deleteRESTHeader,
|
||||
deleteAllRESTHeaders,
|
||||
deleteRESTHeader,
|
||||
restHeaders$,
|
||||
setRESTHeaders,
|
||||
updateRESTHeader,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { commonHeaders } from "~/helpers/headers"
|
||||
import { useSetting } from "~/newstore/settings"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import { HoppRESTHeader } from "~/helpers/types/HoppRESTRequest"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
const bulkMode = ref(false)
|
||||
const bulkHeaders = ref("")
|
||||
const bulkEditor = ref<any | null>(null)
|
||||
|
||||
watch(bulkHeaders, () => {
|
||||
try {
|
||||
const transformation = bulkHeaders.value.split("\n").map((item) => ({
|
||||
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
||||
value: item.substring(item.indexOf(":") + 1).trim(),
|
||||
active: !item.trim().startsWith("//"),
|
||||
}))
|
||||
setRESTHeaders(transformation)
|
||||
} catch (e) {
|
||||
$toast.error(t("error.something_went_wrong").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
headers$: useReadonlyStream(restHeaders$, []),
|
||||
EXPERIMENTAL_URL_BAR_ENABLED: useSetting("EXPERIMENTAL_URL_BAR_ENABLED"),
|
||||
bulkMode,
|
||||
bulkHeaders,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
commonHeaders,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
headers$: {
|
||||
handler(newValue) {
|
||||
if (
|
||||
(newValue[newValue.length - 1]?.key !== "" ||
|
||||
newValue[newValue.length - 1]?.value !== "") &&
|
||||
newValue.length
|
||||
)
|
||||
this.addHeader()
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
// mounted() {
|
||||
// if (!this.headers$?.length) {
|
||||
// this.addHeader()
|
||||
// }
|
||||
// },
|
||||
methods: {
|
||||
addHeader() {
|
||||
addRESTHeader({ key: "", value: "", active: true })
|
||||
},
|
||||
updateHeader(index: number, item: HoppRESTHeader) {
|
||||
updateRESTHeader(index, item)
|
||||
},
|
||||
deleteHeader(index: number) {
|
||||
deleteRESTHeader(index)
|
||||
},
|
||||
clearContent() {
|
||||
deleteAllRESTHeaders()
|
||||
},
|
||||
useCodemirror(bulkEditor, bulkHeaders, {
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: t("state.bulk_mode_placeholder").toString(),
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
|
||||
watch(bulkHeaders, () => {
|
||||
try {
|
||||
const transformation = bulkHeaders.value.split("\n").map((item) => ({
|
||||
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
||||
value: item.substring(item.indexOf(":") + 1).trim(),
|
||||
active: !item.trim().startsWith("//"),
|
||||
}))
|
||||
setRESTHeaders(transformation)
|
||||
} catch (e) {
|
||||
$toast.error(t("error.something_went_wrong").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
const headers$ = useReadonlyStream(restHeaders$, [])
|
||||
|
||||
watch(
|
||||
headers$,
|
||||
(newValue) => {
|
||||
if (
|
||||
(newValue[newValue.length - 1]?.key !== "" ||
|
||||
newValue[newValue.length - 1]?.value !== "") &&
|
||||
newValue.length
|
||||
)
|
||||
addHeader()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const addHeader = () => {
|
||||
addRESTHeader({ key: "", value: "", active: true })
|
||||
}
|
||||
|
||||
const updateHeader = (index: number, item: HoppRESTHeader) => {
|
||||
updateRESTHeader(index, item)
|
||||
}
|
||||
|
||||
const deleteHeader = (index: number) => {
|
||||
deleteRESTHeader(index)
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
deleteAllRESTHeaders()
|
||||
}
|
||||
const EXPERIMENTAL_URL_BAR_ENABLED = useSetting("EXPERIMENTAL_URL_BAR_ENABLED")
|
||||
</script>
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
<template>
|
||||
<SmartModal v-if="show" :title="$t('import.curl')" @close="hideModal">
|
||||
<SmartModal
|
||||
v-if="show"
|
||||
:title="$t('import.curl').toString()"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col px-2">
|
||||
<textarea-autosize
|
||||
id="import-curl"
|
||||
v-model="curl"
|
||||
class="font-mono textarea floating-input"
|
||||
autofocus
|
||||
rows="8"
|
||||
placeholder=" "
|
||||
/>
|
||||
<label for="import-curl">
|
||||
{{ $t("request.enter_curl") }}
|
||||
</label>
|
||||
<div ref="curlEditor" class="border border-dividerLight rounded"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<span>
|
||||
<span class="flex">
|
||||
<ButtonPrimary
|
||||
:label="$t('import.title')"
|
||||
:label="$t('import.title').toString()"
|
||||
@click.native="handleImport"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
:label="$t('action.cancel')"
|
||||
:label="$t('action.cancel').toString()"
|
||||
@click.native="hideModal"
|
||||
/>
|
||||
</span>
|
||||
@@ -30,108 +24,114 @@
|
||||
</SmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
<script setup lang="ts">
|
||||
import { ref, useContext } from "@nuxtjs/composition-api"
|
||||
import parseCurlCommand from "~/helpers/curlparser"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import {
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
makeRESTRequest,
|
||||
} from "~/helpers/types/HoppRESTRequest"
|
||||
import { setRESTRequest } from "~/newstore/RESTSession"
|
||||
import "codemirror/mode/shell/shell"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
show: Boolean,
|
||||
},
|
||||
emits: ["hide-modal"],
|
||||
data() {
|
||||
return {
|
||||
curl: "",
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hideModal() {
|
||||
this.$emit("hide-modal")
|
||||
},
|
||||
handleImport() {
|
||||
const text = this.curl
|
||||
try {
|
||||
const parsedCurl = parseCurlCommand(text)
|
||||
const { origin, pathname } = new URL(
|
||||
parsedCurl.url.replace(/"/g, "").replace(/'/g, "")
|
||||
)
|
||||
const endpoint = origin + pathname
|
||||
const headers: HoppRESTHeader[] = []
|
||||
const params: HoppRESTParam[] = []
|
||||
if (parsedCurl.query) {
|
||||
for (const key of Object.keys(parsedCurl.query)) {
|
||||
const val = parsedCurl.query[key]!
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach((value) => {
|
||||
params.push({
|
||||
key,
|
||||
value,
|
||||
active: true,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
params.push({
|
||||
key,
|
||||
value: val!,
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsedCurl.headers) {
|
||||
for (const key of Object.keys(parsedCurl.headers)) {
|
||||
headers.push({
|
||||
const curl = ref("")
|
||||
|
||||
const curlEditor = ref<any | null>(null)
|
||||
|
||||
useCodemirror(curlEditor, curl, {
|
||||
extendedEditorConfig: {
|
||||
mode: "application/x-sh",
|
||||
placeholder: t("request.enter_curl").toString(),
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
|
||||
defineProps<{ show: boolean }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const handleImport = () => {
|
||||
const text = curl.value
|
||||
try {
|
||||
const parsedCurl = parseCurlCommand(text)
|
||||
const { origin, pathname } = new URL(
|
||||
parsedCurl.url.replace(/"/g, "").replace(/'/g, "")
|
||||
)
|
||||
const endpoint = origin + pathname
|
||||
const headers: HoppRESTHeader[] = []
|
||||
const params: HoppRESTParam[] = []
|
||||
if (parsedCurl.query) {
|
||||
for (const key of Object.keys(parsedCurl.query)) {
|
||||
const val = parsedCurl.query[key]!
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach((value) => {
|
||||
params.push({
|
||||
key,
|
||||
value: parsedCurl.headers[key],
|
||||
value,
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
const method = parsedCurl.method.toUpperCase()
|
||||
// let rawInput = false
|
||||
// let rawParams: any | null = null
|
||||
|
||||
// if (parsedCurl.data) {
|
||||
// rawInput = true
|
||||
// rawParams = parsedCurl.data
|
||||
// }
|
||||
|
||||
this.showCurlImportModal = false
|
||||
|
||||
setRESTRequest(
|
||||
makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
endpoint,
|
||||
method,
|
||||
params,
|
||||
headers,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
contentType: "application/json",
|
||||
body: "",
|
||||
},
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.$toast.error(this.$t("error.curl_invalid_format").toString(), {
|
||||
icon: "error_outline",
|
||||
} else {
|
||||
params.push({
|
||||
key,
|
||||
value: val!,
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsedCurl.headers) {
|
||||
for (const key of Object.keys(parsedCurl.headers)) {
|
||||
headers.push({
|
||||
key,
|
||||
value: parsedCurl.headers[key],
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
this.hideModal()
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
const method = parsedCurl.method.toUpperCase()
|
||||
|
||||
setRESTRequest(
|
||||
makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
endpoint,
|
||||
method,
|
||||
params,
|
||||
headers,
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
body: {
|
||||
contentType: "application/json",
|
||||
body: "",
|
||||
},
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
$toast.error(t("error.curl_invalid_format").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
hideModal()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -47,27 +47,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bulkMode" class="flex">
|
||||
<textarea-autosize
|
||||
v-model="bulkParams"
|
||||
v-focus
|
||||
name="bulk-parameters"
|
||||
class="
|
||||
bg-transparent
|
||||
border-b border-dividerLight
|
||||
flex
|
||||
font-mono font-medium
|
||||
flex-1
|
||||
py-2
|
||||
px-4
|
||||
whitespace-pre
|
||||
resize-y
|
||||
overflow-auto
|
||||
"
|
||||
rows="10"
|
||||
:placeholder="$t('state.bulk_mode_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="bulkMode" ref="bulkEditor"></div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(param, index) in params$"
|
||||
@@ -96,7 +76,7 @@
|
||||
<input
|
||||
v-else
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('count.parameter', { count: index + 1 })"
|
||||
:placeholder="$t('count.parameter', { count: index + 1 }).toString()"
|
||||
:name="'param' + index"
|
||||
:value="param.key"
|
||||
autofocus
|
||||
@@ -130,7 +110,7 @@
|
||||
<input
|
||||
v-else
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('count.value', { count: index + 1 })"
|
||||
:placeholder="$t('count.value', { count: index + 1 }).toString()"
|
||||
:name="'value' + index"
|
||||
:value="param.value"
|
||||
@change="
|
||||
@@ -202,13 +182,9 @@
|
||||
</AppSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
useContext,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api"
|
||||
<script setup lang="ts">
|
||||
import { ref, useContext, watch } from "@nuxtjs/composition-api"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { HoppRESTParam } from "~/helpers/types/HoppRESTRequest"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import {
|
||||
@@ -220,72 +196,74 @@ import {
|
||||
setRESTParams,
|
||||
} from "~/newstore/RESTSession"
|
||||
import { useSetting } from "~/newstore/settings"
|
||||
import "codemirror/mode/yaml/yaml"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const bulkMode = ref(false)
|
||||
const bulkParams = ref("")
|
||||
const bulkMode = ref(false)
|
||||
const bulkParams = ref("")
|
||||
|
||||
watch(bulkParams, () => {
|
||||
try {
|
||||
const transformation = bulkParams.value.split("\n").map((item) => ({
|
||||
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
||||
value: item.substring(item.indexOf(":") + 1).trim(),
|
||||
active: !item.trim().startsWith("//"),
|
||||
}))
|
||||
setRESTParams(transformation)
|
||||
} catch (e) {
|
||||
$toast.error(t("error.something_went_wrong").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
watch(bulkParams, () => {
|
||||
try {
|
||||
const transformation = bulkParams.value.split("\n").map((item) => ({
|
||||
key: item.substring(0, item.indexOf(":")).trim().replace(/^\/\//, ""),
|
||||
value: item.substring(item.indexOf(":") + 1).trim(),
|
||||
active: !item.trim().startsWith("//"),
|
||||
}))
|
||||
setRESTParams(transformation)
|
||||
} catch (e) {
|
||||
$toast.error(t("error.something_went_wrong").toString(), {
|
||||
icon: "error_outline",
|
||||
})
|
||||
|
||||
return {
|
||||
params$: useReadonlyStream(restParams$, []),
|
||||
EXPERIMENTAL_URL_BAR_ENABLED: useSetting("EXPERIMENTAL_URL_BAR_ENABLED"),
|
||||
bulkMode,
|
||||
bulkParams,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
params$: {
|
||||
handler(newValue) {
|
||||
if (
|
||||
(newValue[newValue.length - 1]?.key !== "" ||
|
||||
newValue[newValue.length - 1]?.value !== "") &&
|
||||
newValue.length
|
||||
)
|
||||
this.addParam()
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
// mounted() {
|
||||
// if (!this.params$?.length) {
|
||||
// this.addParam()
|
||||
// }
|
||||
// },
|
||||
methods: {
|
||||
addParam() {
|
||||
addRESTParam({ key: "", value: "", active: true })
|
||||
},
|
||||
updateParam(index: number, item: HoppRESTParam) {
|
||||
updateRESTParam(index, item)
|
||||
},
|
||||
deleteParam(index: number) {
|
||||
deleteRESTParam(index)
|
||||
},
|
||||
clearContent() {
|
||||
deleteAllRESTParams()
|
||||
},
|
||||
},
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
const bulkEditor = ref<any | null>(null)
|
||||
|
||||
useCodemirror(bulkEditor, bulkParams, {
|
||||
extendedEditorConfig: {
|
||||
mode: "text/x-yaml",
|
||||
placeholder: t("state.bulk_mode_placeholder").toString(),
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
|
||||
const params$ = useReadonlyStream(restParams$, [])
|
||||
|
||||
watch(
|
||||
params$,
|
||||
(newValue) => {
|
||||
if (
|
||||
(newValue[newValue.length - 1]?.key !== "" ||
|
||||
newValue[newValue.length - 1]?.value !== "") &&
|
||||
newValue.length
|
||||
)
|
||||
addParam()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const addParam = () => {
|
||||
addRESTParam({ key: "", value: "", active: true })
|
||||
}
|
||||
|
||||
const updateParam = (index: number, item: HoppRESTParam) => {
|
||||
updateRESTParam(index, item)
|
||||
}
|
||||
|
||||
const deleteParam = (index: number) => {
|
||||
deleteRESTParam(index)
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
deleteAllRESTParams()
|
||||
}
|
||||
|
||||
const EXPERIMENTAL_URL_BAR_ENABLED = useSetting("EXPERIMENTAL_URL_BAR_ENABLED")
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="corner-down-left"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.clear')"
|
||||
@@ -34,17 +41,7 @@
|
||||
</div>
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div class="border-r border-dividerLight w-2/3">
|
||||
<SmartJsEditor
|
||||
v-model="preRequestScript"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
complete-mode="pre"
|
||||
/>
|
||||
<div ref="preRrequestEditor"></div>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
@@ -84,29 +81,44 @@
|
||||
</AppSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
|
||||
import { usePreRequestScript } from "~/newstore/RESTSession"
|
||||
import preRequestScriptSnippets from "~/helpers/preRequestScriptSnippets"
|
||||
import snippets from "~/helpers/preRequestScriptSnippets"
|
||||
import "codemirror/mode/javascript/javascript"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import linter from "~/helpers/editor/linting/preRequest"
|
||||
import completer from "~/helpers/editor/completion/preRequest"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const preRequestScript = usePreRequestScript()
|
||||
const {
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const useSnippet = (script: string) => {
|
||||
preRequestScript.value += script
|
||||
}
|
||||
const preRequestScript = usePreRequestScript()
|
||||
|
||||
const clearContent = () => {
|
||||
preRequestScript.value = ""
|
||||
}
|
||||
const preRrequestEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
return {
|
||||
preRequestScript,
|
||||
snippets: preRequestScriptSnippets,
|
||||
useSnippet,
|
||||
clearContent,
|
||||
}
|
||||
},
|
||||
})
|
||||
useCodemirror(
|
||||
preRrequestEditor,
|
||||
preRequestScript,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/javascript",
|
||||
lineWrapping: linewrapEnabled,
|
||||
placeholder: t("preRequest.javascript_code").toString(),
|
||||
},
|
||||
linter,
|
||||
completer,
|
||||
})
|
||||
)
|
||||
|
||||
const useSnippet = (script: string) => {
|
||||
preRequestScript.value += script
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
preRequestScript.value = ""
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="corner-down-left"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.clear')"
|
||||
@@ -55,82 +62,87 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
v-model="rawParamsBody"
|
||||
:lang="rawInputEditorLang"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
</div>
|
||||
<div ref="rawBodyParameters"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, useContext } from "@nuxtjs/composition-api"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { getEditorLangForMimeType } from "~/helpers/editorutils"
|
||||
import { pluckRef } from "~/helpers/utils/composables"
|
||||
import { useRESTRequestBody } from "~/newstore/RESTSession"
|
||||
import "codemirror/mode/yaml/yaml"
|
||||
import "codemirror/mode/xml/xml"
|
||||
import "codemirror/mode/css/css"
|
||||
import "codemirror/mode/htmlmixed/htmlmixed"
|
||||
import "codemirror/mode/javascript/javascript"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
contentType: {
|
||||
type: String,
|
||||
required: true,
|
||||
const props = defineProps<{
|
||||
contentType: string
|
||||
}>()
|
||||
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const rawParamsBody = pluckRef(useRESTRequestBody(), "body")
|
||||
const prettifyIcon = ref("align-left")
|
||||
|
||||
const rawInputEditorLang = computed(() =>
|
||||
getEditorLangForMimeType(props.contentType)
|
||||
)
|
||||
const linewrapEnabled = ref(true)
|
||||
const rawBodyParameters = ref<any | null>(null)
|
||||
|
||||
useCodemirror(
|
||||
rawBodyParameters,
|
||||
rawParamsBody,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
lineWrapping: linewrapEnabled,
|
||||
mode: rawInputEditorLang,
|
||||
placeholder: t("request.raw_body").toString(),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
rawParamsBody: pluckRef(useRESTRequestBody(), "body"),
|
||||
prettifyIcon: "align-left",
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
)
|
||||
|
||||
const clearContent = () => {
|
||||
rawParamsBody.value = ""
|
||||
}
|
||||
|
||||
const uploadPayload = (e: InputEvent) => {
|
||||
const file = e.target.files[0]
|
||||
if (file !== undefined && file !== null) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
rawParamsBody.value = target?.result
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rawInputEditorLang() {
|
||||
return getEditorLangForMimeType(this.contentType)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clearContent() {
|
||||
this.rawParamsBody = ""
|
||||
},
|
||||
uploadPayload() {
|
||||
const file = this.$refs.payload.files[0]
|
||||
if (file !== undefined && file !== null) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = ({ target }) => {
|
||||
this.rawParamsBody = target.result
|
||||
}
|
||||
reader.readAsText(file)
|
||||
this.$toast.success(this.$t("state.file_imported"), {
|
||||
icon: "attach_file",
|
||||
})
|
||||
} else {
|
||||
this.$toast.error(this.$t("action.choose_file"), {
|
||||
icon: "attach_file",
|
||||
})
|
||||
}
|
||||
this.$refs.payload.value = ""
|
||||
},
|
||||
prettifyRequestBody() {
|
||||
try {
|
||||
const jsonObj = JSON.parse(this.rawParamsBody)
|
||||
this.rawParamsBody = JSON.stringify(jsonObj, null, 2)
|
||||
this.prettifyIcon = "check"
|
||||
setTimeout(() => (this.prettifyIcon = "align-left"), 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.$toast.error(`${this.$t("error.json_prettify_invalid_body")}`, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
reader.readAsText(file)
|
||||
$toast.success(t("state.file_imported").toString(), {
|
||||
icon: "attach_file",
|
||||
})
|
||||
} else {
|
||||
$toast.error(t("action.choose_file").toString(), {
|
||||
icon: "attach_file",
|
||||
})
|
||||
}
|
||||
}
|
||||
const prettifyRequestBody = () => {
|
||||
try {
|
||||
const jsonObj = JSON.parse(rawParamsBody.value)
|
||||
rawParamsBody.value = JSON.stringify(jsonObj, null, 2)
|
||||
prettifyIcon.value = "check"
|
||||
setTimeout(() => (prettifyIcon.value = "align-left"), 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
$toast.error(`${t("error.json_prettify_invalid_body")}`, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
"
|
||||
>
|
||||
<div class="flex space-x-2 pb-4">
|
||||
<div class="flex flex-col space-y-4 items-end">
|
||||
<div class="flex flex-col space-y-4 text-right items-end">
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.request.send_request") }}
|
||||
</span>
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.show_all") }}
|
||||
</span>
|
||||
<!-- <span class="flex flex-1 items-center">
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.command_menu") }}
|
||||
</span>
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ $t("shortcut.general.help_menu") }}
|
||||
</span> -->
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex">
|
||||
@@ -33,12 +33,12 @@
|
||||
<span class="shortcut-key">{{ getSpecialKey() }}</span>
|
||||
<span class="shortcut-key">K</span>
|
||||
</div>
|
||||
<!-- <div class="flex">
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">/</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span class="shortcut-key">?</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonSecondary
|
||||
@@ -102,26 +102,23 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
<script setup lang="ts">
|
||||
import { computed } from "@nuxtjs/composition-api"
|
||||
import findStatusGroup from "~/helpers/findStatusGroup"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
response: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
statusCategory() {
|
||||
return findStatusGroup(this.response.statusCode)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getSpecialKey: getPlatformSpecialKey,
|
||||
},
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse
|
||||
}>()
|
||||
|
||||
const statusCategory = computed(() => {
|
||||
if (
|
||||
props.response.type === "loading" ||
|
||||
props.response.type === "network_fail"
|
||||
)
|
||||
return ""
|
||||
return findStatusGroup(props.response.statusCode)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="corner-down-left"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.clear')"
|
||||
@@ -34,17 +41,7 @@
|
||||
</div>
|
||||
<div class="border-b border-dividerLight flex">
|
||||
<div class="border-r border-dividerLight w-2/3">
|
||||
<SmartJsEditor
|
||||
v-model="testScript"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
complete-mode="test"
|
||||
/>
|
||||
<div ref="testScriptEditor"></div>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
@@ -85,11 +82,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, useContext } from "@nuxtjs/composition-api"
|
||||
import { useTestScript } from "~/newstore/RESTSession"
|
||||
import testSnippets from "~/helpers/testSnippets"
|
||||
import "codemirror/mode/javascript/javascript"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import linter from "~/helpers/editor/linting/testScript"
|
||||
import completer from "~/helpers/editor/completion/testScript"
|
||||
|
||||
const {
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const testScript = useTestScript()
|
||||
|
||||
const testScriptEditor = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
testScriptEditor,
|
||||
testScript,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/javascript",
|
||||
lineWrapping: linewrapEnabled,
|
||||
placeholder: t("test.javascript_code").toString(),
|
||||
},
|
||||
linter,
|
||||
completer,
|
||||
})
|
||||
)
|
||||
|
||||
const useSnippet = (script: string) => {
|
||||
testScript.value += script
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
group-hover:text-secondaryDark
|
||||
"
|
||||
>
|
||||
<span class="rounded select-all truncate">
|
||||
<span class="rounded-sm select-all truncate">
|
||||
{{ header.key }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -61,7 +61,7 @@
|
||||
group-hover:text-secondaryDark
|
||||
"
|
||||
>
|
||||
<span class="rounded select-all truncate">
|
||||
<span class="rounded-sm select-all truncate">
|
||||
{{ header.value }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
{{ $t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="corner-down-left"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -44,110 +52,131 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
:value="responseBodyText"
|
||||
:lang="'html'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
<iframe
|
||||
ref="previewFrame"
|
||||
:class="{ hidden: !previewEnabled }"
|
||||
class="covers-response"
|
||||
src="about:blank"
|
||||
></iframe>
|
||||
</div>
|
||||
<div v-show="!previewEnabled" ref="htmlResponse"></div>
|
||||
<iframe
|
||||
v-show="previewEnabled"
|
||||
ref="previewFrame"
|
||||
class="covers-response"
|
||||
src="about:blank"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import "codemirror/mode/xml/xml"
|
||||
import "codemirror/mode/javascript/javascript"
|
||||
import "codemirror/mode/css/css"
|
||||
import "codemirror/mode/htmlmixed/htmlmixed"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [TextContentRendererMixin],
|
||||
props: {
|
||||
response: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
downloadIcon: "download",
|
||||
copyIcon: "copy",
|
||||
previewEnabled: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
downloadResponse() {
|
||||
const dataToWrite = this.responseBodyText
|
||||
const file = new Blob([dataToWrite], { type: "text/html" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.downloadIcon = "check"
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
this.downloadIcon = "download"
|
||||
}, 1000)
|
||||
},
|
||||
copyResponse() {
|
||||
copyToClipboard(this.responseBodyText)
|
||||
this.copyIcon = "check"
|
||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
||||
},
|
||||
togglePreview() {
|
||||
this.previewEnabled = !this.previewEnabled
|
||||
if (this.previewEnabled) {
|
||||
if (
|
||||
this.$refs.previewFrame.getAttribute("data-previewing-url") ===
|
||||
this.url
|
||||
)
|
||||
return
|
||||
// Use DOMParser to parse document HTML.
|
||||
const previewDocument = new DOMParser().parseFromString(
|
||||
this.responseBodyText,
|
||||
"text/html"
|
||||
)
|
||||
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
|
||||
previewDocument.head.innerHTML =
|
||||
`<base href="${this.url}">` + previewDocument.head.innerHTML
|
||||
// Finally, set the iframe source to the resulting HTML.
|
||||
this.$refs.previewFrame.srcdoc =
|
||||
previewDocument.documentElement.outerHTML
|
||||
this.$refs.previewFrame.setAttribute("data-previewing-url", this.url)
|
||||
}
|
||||
},
|
||||
},
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse
|
||||
}>()
|
||||
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const responseBodyText = computed(() => {
|
||||
if (
|
||||
props.response.type === "loading" ||
|
||||
props.response.type === "network_fail"
|
||||
)
|
||||
return ""
|
||||
if (typeof props.response.body === "string") return props.response.body
|
||||
else {
|
||||
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
}
|
||||
})
|
||||
|
||||
const downloadIcon = ref("download")
|
||||
const copyIcon = ref("copy")
|
||||
const previewEnabled = ref(false)
|
||||
const previewFrame = ref<any | null>(null)
|
||||
const url = ref("")
|
||||
|
||||
const htmlResponse = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
htmlResponse,
|
||||
responseBodyText,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "htmlmixed",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
)
|
||||
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseBodyText.value
|
||||
const file = new Blob([dataToWrite], { type: "text/html" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseBodyText.value)
|
||||
copyIcon.value = "check"
|
||||
$toast.success(t("state.copied_to_clipboard").toString(), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const togglePreview = () => {
|
||||
previewEnabled.value = !previewEnabled.value
|
||||
if (previewEnabled.value) {
|
||||
if (previewFrame.value.getAttribute("data-previewing-url") === url.value)
|
||||
return
|
||||
// Use DOMParser to parse document HTML.
|
||||
const previewDocument = new DOMParser().parseFromString(
|
||||
responseBodyText.value,
|
||||
"text/html"
|
||||
)
|
||||
// Inject <base href="..."> tag to head, to fix relative CSS/HTML paths.
|
||||
previewDocument.head.innerHTML =
|
||||
`<base href="${url.value}">` + previewDocument.head.innerHTML
|
||||
// Finally, set the iframe source to the resulting HTML.
|
||||
previewFrame.value.srcdoc = previewDocument.documentElement.outerHTML
|
||||
previewFrame.value.setAttribute("data-previewing-url", url.value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.covers-response {
|
||||
@apply absolute;
|
||||
@apply inset-0;
|
||||
@apply bg-white;
|
||||
@apply min-h-64;
|
||||
@apply h-full;
|
||||
@apply w-full;
|
||||
@apply border;
|
||||
@apply border-dividerLight;
|
||||
@apply z-5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,12 +27,10 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex relative">
|
||||
<img
|
||||
class="border-b border-dividerLight flex max-w-full flex-1"
|
||||
:src="imageSource"
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
class="border-b border-dividerLight flex max-w-full flex-1"
|
||||
:src="imageSource"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -13,10 +13,18 @@
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("response.body") }}
|
||||
</label>
|
||||
<label class="font-semibold text-secondaryLight">{{
|
||||
$t("response.body")
|
||||
}}</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="corner-down-left"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="downloadResponse"
|
||||
@@ -35,89 +43,237 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
:value="jsonBodyText"
|
||||
:lang="'json'"
|
||||
:provide-outline="true"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
<div ref="jsonResponse"></div>
|
||||
<div
|
||||
v-if="outlinePath"
|
||||
class="
|
||||
bg-primaryLight
|
||||
border-t border-dividerLight
|
||||
flex flex-nowrap flex-1
|
||||
px-2
|
||||
bottom-0
|
||||
z-10
|
||||
sticky
|
||||
overflow-auto
|
||||
hide-scrollbar
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in outlinePath"
|
||||
:key="`item-${index}`"
|
||||
class="flex items-center"
|
||||
>
|
||||
<tippy
|
||||
ref="outlineOptions"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<div v-if="item.kind === 'RootObject'" class="outline">{}</div>
|
||||
<div v-if="item.kind === 'RootArray'" class="outline">[]</div>
|
||||
<div v-if="item.kind === 'ArrayMember'" class="outline">
|
||||
{{ item.index.toString() }}
|
||||
</div>
|
||||
<div v-if="item.kind === 'ObjectMember'" class="outline">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-if="item.kind === 'ArrayMember' || item.kind === 'ObjectMember'"
|
||||
>
|
||||
<div v-if="item.kind === 'ArrayMember'" class="flex flex-col">
|
||||
<SmartItem
|
||||
v-for="(arrayMember, astIndex) in item.astParent.values"
|
||||
:key="`ast-${astIndex}`"
|
||||
:label="astIndex.toString()"
|
||||
@click.native="
|
||||
() => {
|
||||
jumpCursor(arrayMember)
|
||||
outlineOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="item.kind === 'ObjectMember'" class="flex flex-col">
|
||||
<SmartItem
|
||||
v-for="(objectMember, astIndex) in item.astParent.members"
|
||||
:key="`ast-${astIndex}`"
|
||||
:label="objectMember.key.value"
|
||||
@click.native="
|
||||
() => {
|
||||
jumpCursor(objectMember)
|
||||
outlineOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.kind === 'RootObject'" class="flex flex-col">
|
||||
<SmartItem
|
||||
label="{}"
|
||||
@click.native="
|
||||
() => {
|
||||
jumpCursor(item.astValue)
|
||||
outlineOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="item.kind === 'RootArray'" class="flex flex-col">
|
||||
<SmartItem
|
||||
label="[]"
|
||||
@click.native="
|
||||
() => {
|
||||
jumpCursor(item.astValue)
|
||||
outlineOptions[index].tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</tippy>
|
||||
<i
|
||||
v-if="index + 1 !== outlinePath.length"
|
||||
class="text-secondaryLight opacity-50 material-icons"
|
||||
>chevron_right</i
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import "codemirror/mode/javascript/javascript"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import jsonParse, { JSONObjectMember, JSONValue } from "~/helpers/jsonParse"
|
||||
import { getJSONOutlineAtPos } from "~/helpers/newOutline"
|
||||
import {
|
||||
convertIndexToLineCh,
|
||||
convertLineChToIndex,
|
||||
} from "~/helpers/editor/utils"
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [TextContentRendererMixin],
|
||||
props: {
|
||||
response: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
downloadIcon: "download",
|
||||
copyIcon: "copy",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
jsonBodyText() {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(this.responseBodyText), null, 2)
|
||||
} catch (e) {
|
||||
// Most probs invalid JSON was returned, so drop prettification (should we warn ?)
|
||||
return this.responseBodyText
|
||||
}
|
||||
},
|
||||
responseType() {
|
||||
return (
|
||||
this.response.headers.find(
|
||||
(h) => h.key.toLowerCase() === "content-type"
|
||||
).value || ""
|
||||
)
|
||||
.split(";")[0]
|
||||
.toLowerCase()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downloadResponse() {
|
||||
const dataToWrite = this.responseBodyText
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.downloadIcon = "check"
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
this.downloadIcon = "download"
|
||||
}, 1000)
|
||||
},
|
||||
copyResponse() {
|
||||
copyToClipboard(this.responseBodyText)
|
||||
this.copyIcon = "check"
|
||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
||||
},
|
||||
},
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse
|
||||
}>()
|
||||
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const responseBodyText = computed(() => {
|
||||
if (
|
||||
props.response.type === "loading" ||
|
||||
props.response.type === "network_fail"
|
||||
)
|
||||
return ""
|
||||
if (typeof props.response.body === "string") return props.response.body
|
||||
else {
|
||||
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
}
|
||||
})
|
||||
|
||||
const downloadIcon = ref("download")
|
||||
const copyIcon = ref("copy")
|
||||
|
||||
const jsonBodyText = computed(() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(responseBodyText.value), null, 2)
|
||||
} catch (e) {
|
||||
// Most probs invalid JSON was returned, so drop prettification (should we warn ?)
|
||||
return responseBodyText.value
|
||||
}
|
||||
})
|
||||
|
||||
const ast = computed(() => {
|
||||
try {
|
||||
return jsonParse(jsonBodyText.value)
|
||||
} catch (_: any) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const outlineOptions = ref<any | null>(null)
|
||||
const jsonResponse = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const { cursor } = useCodemirror(
|
||||
jsonResponse,
|
||||
jsonBodyText,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/ld+json",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
)
|
||||
|
||||
const jumpCursor = (ast: JSONValue | JSONObjectMember) => {
|
||||
const pos = convertIndexToLineCh(jsonBodyText.value, ast.start)
|
||||
pos.line--
|
||||
cursor.value = pos
|
||||
}
|
||||
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseBodyText.value
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const outlinePath = computed(() => {
|
||||
if (ast.value) {
|
||||
return getJSONOutlineAtPos(
|
||||
ast.value,
|
||||
convertLineChToIndex(jsonBodyText.value, cursor.value)
|
||||
)
|
||||
} else return null
|
||||
})
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseBodyText.value)
|
||||
copyIcon.value = "check"
|
||||
$toast.success(t("state.copied_to_clipboard").toString(), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.outline {
|
||||
@apply cursor-pointer;
|
||||
@apply flex-grow-0 flex-shrink-0;
|
||||
@apply text-secondaryLight;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
@apply px-2;
|
||||
@apply py-1;
|
||||
@apply transition;
|
||||
@apply hover:text-secondary;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
{{ $t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="corner-down-left"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="downloadResponse"
|
||||
@@ -35,80 +43,96 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
:value="responseBodyText"
|
||||
:lang="'plain_text'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
</div>
|
||||
<div ref="rawResponse"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
||||
<script setup lang="ts">
|
||||
import { ref, useContext, computed, reactive } from "@nuxtjs/composition-api"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [TextContentRendererMixin],
|
||||
props: {
|
||||
response: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
downloadIcon: "download",
|
||||
copyIcon: "copy",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
responseType() {
|
||||
return (
|
||||
this.response.headers.find(
|
||||
(h) => h.key.toLowerCase() === "content-type"
|
||||
).value || ""
|
||||
)
|
||||
.split(";")[0]
|
||||
.toLowerCase()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downloadResponse() {
|
||||
const dataToWrite = this.responseBodyText
|
||||
const file = new Blob([dataToWrite], { type: this.responseType })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.downloadIcon = "check"
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
this.downloadIcon = "download"
|
||||
}, 1000)
|
||||
},
|
||||
copyResponse() {
|
||||
copyToClipboard(this.responseBodyText)
|
||||
this.copyIcon = "check"
|
||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
||||
},
|
||||
},
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse
|
||||
}>()
|
||||
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const responseBodyText = computed(() => {
|
||||
if (
|
||||
props.response.type === "loading" ||
|
||||
props.response.type === "network_fail"
|
||||
)
|
||||
return ""
|
||||
if (typeof props.response.body === "string") return props.response.body
|
||||
else {
|
||||
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
}
|
||||
})
|
||||
|
||||
const downloadIcon = ref("download")
|
||||
const copyIcon = ref("copy")
|
||||
|
||||
const responseType = computed(() => {
|
||||
return (
|
||||
props.response.headers.find((h) => h.key.toLowerCase() === "content-type")
|
||||
.value || ""
|
||||
)
|
||||
.split(";")[0]
|
||||
.toLowerCase()
|
||||
})
|
||||
|
||||
const rawResponse = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
rawResponse,
|
||||
responseBodyText,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
)
|
||||
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseBodyText.value
|
||||
const file = new Blob([dataToWrite], { type: responseType.value })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseBodyText.value)
|
||||
copyIcon.value = "check"
|
||||
$toast.success(t("state.copied_to_clipboard").toString(), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
{{ $t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="corner-down-left"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="response.body"
|
||||
ref="downloadResponse"
|
||||
@@ -35,80 +43,97 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<SmartAceEditor
|
||||
:value="responseBodyText"
|
||||
:lang="'xml'"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
readOnly: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
styles="border-b border-dividerLight"
|
||||
/>
|
||||
</div>
|
||||
<div ref="xmlResponse"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import TextContentRendererMixin from "./mixins/TextContentRendererMixin"
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useContext, reactive } from "@nuxtjs/composition-api"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import "codemirror/mode/xml/xml"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [TextContentRendererMixin],
|
||||
props: {
|
||||
response: { type: Object, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
copyIcon: "copy",
|
||||
downloadIcon: "download",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
responseType() {
|
||||
return (
|
||||
this.response.headers.find(
|
||||
(h) => h.key.toLowerCase() === "content-type"
|
||||
).value || ""
|
||||
)
|
||||
.split(";")[0]
|
||||
.toLowerCase()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downloadResponse() {
|
||||
const dataToWrite = this.responseBodyText
|
||||
const file = new Blob([dataToWrite], { type: this.responseType })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
this.downloadIcon = "check"
|
||||
this.$toast.success(this.$t("state.download_started"), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
this.downloadIcon = "download"
|
||||
}, 1000)
|
||||
},
|
||||
copyResponse() {
|
||||
copyToClipboard(this.responseBodyText)
|
||||
this.copyIcon = "check"
|
||||
this.$toast.success(this.$t("state.copied_to_clipboard"), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (this.copyIcon = "copy"), 1000)
|
||||
},
|
||||
},
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse
|
||||
}>()
|
||||
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const responseBodyText = computed(() => {
|
||||
if (
|
||||
props.response.type === "loading" ||
|
||||
props.response.type === "network_fail"
|
||||
)
|
||||
return ""
|
||||
if (typeof props.response.body === "string") return props.response.body
|
||||
else {
|
||||
const res = new TextDecoder("utf-8").decode(props.response.body)
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
}
|
||||
})
|
||||
|
||||
const downloadIcon = ref("download")
|
||||
const copyIcon = ref("copy")
|
||||
|
||||
const responseType = computed(() => {
|
||||
return (
|
||||
props.response.headers.find((h) => h.key.toLowerCase() === "content-type")
|
||||
.value || ""
|
||||
)
|
||||
.split(";")[0]
|
||||
.toLowerCase()
|
||||
})
|
||||
|
||||
const xmlResponse = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
xmlResponse,
|
||||
responseBodyText,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "application/xml",
|
||||
readOnly: true,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
})
|
||||
)
|
||||
|
||||
const downloadResponse = () => {
|
||||
const dataToWrite = responseBodyText.value
|
||||
const file = new Blob([dataToWrite], { type: responseType.value })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
// TODO get uri from meta
|
||||
a.download = `${url.split("/").pop().split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadIcon.value = "check"
|
||||
$toast.success(t("state.download_started").toString(), {
|
||||
icon: "downloading",
|
||||
})
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
downloadIcon.value = "download"
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const copyResponse = () => {
|
||||
copyToClipboard(responseBodyText.value)
|
||||
copyIcon.value = "check"
|
||||
$toast.success(t("state.copied_to_clipboard").toString(), {
|
||||
icon: "content_paste",
|
||||
})
|
||||
setTimeout(() => (copyIcon.value = "copy"), 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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) {
|
||||
switch (event.code) {
|
||||
case "Enter":
|
||||
event.preventDefault()
|
||||
if (this.currentSuggestionIndex > -1)
|
||||
this.forceSuggestion(
|
||||
this.suggestions.find(
|
||||
(_item, index) => index === this.currentSuggestionIndex
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
case "ArrowUp":
|
||||
event.preventDefault()
|
||||
this.currentSuggestionIndex =
|
||||
|
||||
@@ -483,7 +483,7 @@ export default defineComponent({
|
||||
line-height: 1.9;
|
||||
|
||||
&::before {
|
||||
@apply text-secondaryDark;
|
||||
@apply text-secondary;
|
||||
@apply opacity-25;
|
||||
@apply pointer-events-none;
|
||||
|
||||
@@ -501,7 +501,6 @@ export default defineComponent({
|
||||
@apply overflow-y-hidden;
|
||||
@apply resize-none;
|
||||
@apply focus:outline-none;
|
||||
@apply transition;
|
||||
}
|
||||
|
||||
.env-input::-webkit-scrollbar {
|
||||
|
||||
@@ -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 }))
|
||||
: request.effectiveFinalHeaders.map((x) => ({ ...x, active: true }))
|
||||
|
||||
console.log(finalHeaders)
|
||||
|
||||
return {
|
||||
name: request.name,
|
||||
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 = {
|
||||
"text/plain": "plain_text",
|
||||
"text/html": "html",
|
||||
"application/xml": "xml",
|
||||
"application/hal+json": "json",
|
||||
"application/vnd.api+json": "json",
|
||||
"application/json": "json",
|
||||
"text/plain": "text/x-yaml",
|
||||
"text/html": "htmlmixed",
|
||||
"application/xml": "application/xml",
|
||||
"application/hal+json": "application/ld+json",
|
||||
"application/vnd.api+json": "application/ld+json",
|
||||
"application/json": "application/ld+json",
|
||||
}
|
||||
|
||||
export function getEditorLangForMimeType(mimeType) {
|
||||
return mimeToMode[mimeType] || "plain_text"
|
||||
return mimeToMode[mimeType] || "text/x-yaml"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,75 @@
|
||||
* - end: int - the end exclusive offset of the syntax error
|
||||
*
|
||||
*/
|
||||
export default function jsonParse(str) {
|
||||
type JSONEOFValue = {
|
||||
kind: "EOF"
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
type JSONNullValue = {
|
||||
kind: "Null"
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
type JSONNumberValue = {
|
||||
kind: "Number"
|
||||
start: number
|
||||
end: number
|
||||
value: number
|
||||
}
|
||||
|
||||
type JSONStringValue = {
|
||||
kind: "String"
|
||||
start: number
|
||||
end: number
|
||||
value: string
|
||||
}
|
||||
|
||||
type JSONBooleanValue = {
|
||||
kind: "Boolean"
|
||||
start: number
|
||||
end: number
|
||||
value: boolean
|
||||
}
|
||||
|
||||
type JSONPrimitiveValue =
|
||||
| JSONNullValue
|
||||
| JSONEOFValue
|
||||
| JSONStringValue
|
||||
| JSONNumberValue
|
||||
| JSONBooleanValue
|
||||
|
||||
export type JSONObjectValue = {
|
||||
kind: "Object"
|
||||
start: number
|
||||
end: number
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
members: JSONObjectMember[]
|
||||
}
|
||||
|
||||
export type JSONArrayValue = {
|
||||
kind: "Array"
|
||||
start: number
|
||||
end: number
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
values: JSONValue[]
|
||||
}
|
||||
|
||||
export type JSONValue = JSONObjectValue | JSONArrayValue | JSONPrimitiveValue
|
||||
|
||||
export type JSONObjectMember = {
|
||||
kind: "Member"
|
||||
start: number
|
||||
end: number
|
||||
key: JSONStringValue
|
||||
value: JSONValue
|
||||
}
|
||||
|
||||
export default function jsonParse(
|
||||
str: string
|
||||
): JSONObjectValue | JSONArrayValue {
|
||||
string = str
|
||||
strLen = str.length
|
||||
start = end = lastEnd = -1
|
||||
@@ -37,15 +105,15 @@ export default function jsonParse(str) {
|
||||
}
|
||||
}
|
||||
|
||||
let string
|
||||
let strLen
|
||||
let start
|
||||
let end
|
||||
let lastEnd
|
||||
let code
|
||||
let kind
|
||||
let string: string
|
||||
let strLen: number
|
||||
let start: number
|
||||
let end: number
|
||||
let lastEnd: number
|
||||
let code: number
|
||||
let kind: string
|
||||
|
||||
function parseObj() {
|
||||
function parseObj(): JSONObjectValue {
|
||||
const nodeStart = start
|
||||
const members = []
|
||||
expect("{")
|
||||
@@ -63,9 +131,9 @@ function parseObj() {
|
||||
}
|
||||
}
|
||||
|
||||
function parseMember() {
|
||||
function parseMember(): JSONObjectMember {
|
||||
const nodeStart = start
|
||||
const key = kind === "String" ? curToken() : null
|
||||
const key = kind === "String" ? (curToken() as JSONStringValue) : null
|
||||
expect("String")
|
||||
expect(":")
|
||||
const value = parseVal()
|
||||
@@ -73,14 +141,14 @@ function parseMember() {
|
||||
kind: "Member",
|
||||
start: nodeStart,
|
||||
end: lastEnd,
|
||||
key,
|
||||
key: key!,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
function parseArr() {
|
||||
function parseArr(): JSONArrayValue {
|
||||
const nodeStart = start
|
||||
const values = []
|
||||
const values: JSONValue[] = []
|
||||
expect("[")
|
||||
if (!skip("]")) {
|
||||
do {
|
||||
@@ -96,7 +164,7 @@ function parseArr() {
|
||||
}
|
||||
}
|
||||
|
||||
function parseVal() {
|
||||
function parseVal(): JSONValue {
|
||||
switch (kind) {
|
||||
case "[":
|
||||
return parseArr()
|
||||
@@ -111,14 +179,19 @@ function parseVal() {
|
||||
lex()
|
||||
return token
|
||||
}
|
||||
return expect("Value")
|
||||
return expect("Value") as never
|
||||
}
|
||||
|
||||
function curToken() {
|
||||
return { kind, start, end, value: JSON.parse(string.slice(start, end)) }
|
||||
function curToken(): JSONPrimitiveValue {
|
||||
return {
|
||||
kind: kind as any,
|
||||
start,
|
||||
end,
|
||||
value: JSON.parse(string.slice(start, end)),
|
||||
}
|
||||
}
|
||||
|
||||
function expect(str) {
|
||||
function expect(str: string) {
|
||||
if (kind === str) {
|
||||
lex()
|
||||
return
|
||||
@@ -137,11 +210,17 @@ function expect(str) {
|
||||
throw syntaxError(`Expected ${str} but found ${found}.`)
|
||||
}
|
||||
|
||||
function syntaxError(message) {
|
||||
type SyntaxError = {
|
||||
message: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
function syntaxError(message: string): SyntaxError {
|
||||
return { message, start, end }
|
||||
}
|
||||
|
||||
function skip(k) {
|
||||
function skip(k: string) {
|
||||
if (kind === k) {
|
||||
lex()
|
||||
return true
|
||||
@@ -227,7 +306,7 @@ function lex() {
|
||||
function readString() {
|
||||
ch()
|
||||
while (code !== 34 && code > 31) {
|
||||
if (code === 92) {
|
||||
if (code === (92 as any)) {
|
||||
// \
|
||||
ch()
|
||||
switch (code) {
|
||||
@@ -299,7 +378,7 @@ function readNumber() {
|
||||
if (code === 69 || code === 101) {
|
||||
// E e
|
||||
ch()
|
||||
if (code === 43 || code === 45) {
|
||||
if (code === (43 as any) || code === (45 as any)) {
|
||||
// + -
|
||||
ch()
|
||||
}
|
||||
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">
|
||||
<main class="flex flex-1 w-full">
|
||||
<nuxt class="flex flex-1" />
|
||||
<nuxt class="flex overflow-y-auto flex-1" />
|
||||
</main>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
@@ -421,6 +421,7 @@
|
||||
"file_imported": "File imported",
|
||||
"finished_in": "Finished in {duration}ms",
|
||||
"history_deleted": "History deleted",
|
||||
"linewrap": "Wrap lines",
|
||||
"loading": "Loading...",
|
||||
"none": "None",
|
||||
"nothing_found": "Nothing found for",
|
||||
|
||||
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",
|
||||
// https://github.com/antfu/unplugin-vue2-script-setup
|
||||
"unplugin-vue2-script-setup/nuxt",
|
||||
"~/modules/emit-volar-types.ts",
|
||||
],
|
||||
|
||||
// Modules (https://go.nuxtjs.dev/config-modules)
|
||||
@@ -280,7 +281,7 @@ export default {
|
||||
config.module.rules.push({
|
||||
test: /\.js$/,
|
||||
include: /(node_modules)/,
|
||||
exclude: /(node_modules)\/(ace-builds)|(@firebase)/,
|
||||
exclude: /(node_modules)\/(@firebase)/,
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
plugins: [
|
||||
|
||||
102
package-lock.json
generated
102
package-lock.json
generated
@@ -15,15 +15,17 @@
|
||||
"@nuxtjs/robots": "^2.5.0",
|
||||
"@nuxtjs/sitemap": "^2.4.0",
|
||||
"@nuxtjs/toast": "^3.3.1",
|
||||
"ace-builds": "^1.4.12",
|
||||
"acorn": "^8.5.0",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"codemirror": "^5.62.3",
|
||||
"codemirror-theme-github": "^1.0.0",
|
||||
"core-js": "^3.17.3",
|
||||
"esprima": "^4.0.1",
|
||||
"firebase": "^9.0.2",
|
||||
"fuse.js": "^6.4.6",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-language-service-interface": "^2.8.4",
|
||||
"graphql-language-service-parser": "^1.9.2",
|
||||
"json-loader": "^0.5.7",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
@@ -60,7 +62,9 @@
|
||||
"@nuxtjs/stylelint-module": "^4.0.0",
|
||||
"@nuxtjs/svg": "^0.2.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@types/codemirror": "^5.60.2",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/esprima": "^4.0.3",
|
||||
"@types/lodash": "^4.14.172",
|
||||
"@types/splitpanes": "^2.2.1",
|
||||
"@vue/runtime-dom": "^3.2.11",
|
||||
@@ -7915,6 +7919,15 @@
|
||||
"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": {
|
||||
"version": "1.2.10",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.8.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-4.2.1.tgz",
|
||||
@@ -9406,11 +9443,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": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
|
||||
@@ -13144,6 +13176,16 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"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": {
|
||||
"version": "1.2.10",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.8.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-4.2.1.tgz",
|
||||
@@ -43810,11 +43885,6 @@
|
||||
"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": {
|
||||
"version": "8.5.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"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/sitemap": "^2.4.0",
|
||||
"@nuxtjs/toast": "^3.3.1",
|
||||
"ace-builds": "^1.4.12",
|
||||
"acorn": "^8.5.0",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"codemirror": "^5.62.3",
|
||||
"codemirror-theme-github": "^1.0.0",
|
||||
"core-js": "^3.17.3",
|
||||
"esprima": "^4.0.1",
|
||||
"firebase": "^9.0.2",
|
||||
"fuse.js": "^6.4.6",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-language-service-interface": "^2.8.4",
|
||||
"graphql-language-service-parser": "^1.9.2",
|
||||
"json-loader": "^0.5.7",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
@@ -76,7 +78,9 @@
|
||||
"@nuxtjs/stylelint-module": "^4.0.0",
|
||||
"@nuxtjs/svg": "^0.2.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@types/codemirror": "^5.60.2",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/esprima": "^4.0.3",
|
||||
"@types/lodash": "^4.14.172",
|
||||
"@types/splitpanes": "^2.2.1",
|
||||
"@vue/runtime-dom": "^3.2.11",
|
||||
|
||||
@@ -61,17 +61,12 @@
|
||||
@click.native="collectionJSON = '[]'"
|
||||
/>
|
||||
</div>
|
||||
<SmartAceEditor
|
||||
<textarea-autosize
|
||||
id="import-curl"
|
||||
v-model="collectionJSON"
|
||||
:lang="'json'"
|
||||
:lint="false"
|
||||
:options="{
|
||||
maxLines: Infinity,
|
||||
minLines: 16,
|
||||
autoScrollEditorIntoView: true,
|
||||
showPrintMargin: false,
|
||||
useWorker: false,
|
||||
}"
|
||||
class="font-mono p-4 bg-primary"
|
||||
autofocus
|
||||
rows="8"
|
||||
/>
|
||||
<div
|
||||
class="
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:dbl-click-splitter="false"
|
||||
:horizontal="!(windowInnerWidth.x.value >= 768)"
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:dbl-click-splitter="false"
|
||||
:horizontal="!(windowInnerWidth.x.value >= 768)"
|
||||
>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<Splitpanes class="smart-splitter" :dbl-click-splitter="false" horizontal>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<GraphqlRequest :conn="gqlConn" />
|
||||
<GraphqlRequestOptions :conn="gqlConn" />
|
||||
</Pane>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<GraphqlResponse :conn="gqlConn" />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
<Pane
|
||||
v-if="RIGHT_SIDEBAR"
|
||||
max-size="35"
|
||||
size="25"
|
||||
min-size="20"
|
||||
class="hide-scrollbar !overflow-auto"
|
||||
>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:dbl-click-splitter="false"
|
||||
horizontal
|
||||
>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<GraphqlRequest :conn="gqlConn" />
|
||||
<GraphqlRequestOptions :conn="gqlConn" />
|
||||
</Pane>
|
||||
<Pane class="hide-scrollbar !overflow-auto">
|
||||
<GraphqlResponse :conn="gqlConn" />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
<Pane
|
||||
v-if="RIGHT_SIDEBAR"
|
||||
max-size="35"
|
||||
size="25"
|
||||
min-size="20"
|
||||
class="hide-scrollbar !overflow-auto"
|
||||
>
|
||||
<GraphqlSidebar :conn="gqlConn" />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
<GraphqlSidebar :conn="gqlConn" />
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2 py-4 items-center">
|
||||
<div class="flex flex-1 items-center relative">
|
||||
<div class="flex flex-1 flex-col relative">
|
||||
<input
|
||||
id="url"
|
||||
v-model="PROXY_URL"
|
||||
|
||||
Reference in New Issue
Block a user