refactor: monorepo+pnpm (removed husky)
This commit is contained in:
95
packages/hoppscotch-app/components/graphql/Field.vue
Normal file
95
packages/hoppscotch-app/components/graphql/Field.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="field-title" :class="{ 'field-highlighted': isHighlighted }">
|
||||
{{ fieldName }}
|
||||
<span v-if="fieldArgs.length > 0">
|
||||
(
|
||||
<span v-for="(field, index) in fieldArgs" :key="`field-${index}`">
|
||||
{{ field.name }}:
|
||||
<GraphqlTypeLink
|
||||
:gql-type="field.type"
|
||||
:jump-type-callback="jumpTypeCallback"
|
||||
/>
|
||||
<span v-if="index !== fieldArgs.length - 1">, </span>
|
||||
</span>
|
||||
) </span
|
||||
>:
|
||||
<GraphqlTypeLink
|
||||
:gql-type="gqlField.type"
|
||||
:jump-type-callback="jumpTypeCallback"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="gqlField.description"
|
||||
class="text-secondaryLight py-2 field-desc"
|
||||
>
|
||||
{{ gqlField.description }}
|
||||
</div>
|
||||
<div
|
||||
v-if="gqlField.isDeprecated"
|
||||
class="
|
||||
rounded
|
||||
bg-yellow-200
|
||||
my-1
|
||||
text-black
|
||||
py-1
|
||||
px-2
|
||||
inline-block
|
||||
field-deprecated
|
||||
"
|
||||
>
|
||||
{{ $t("state.deprecated") }}
|
||||
</div>
|
||||
<div v-if="fieldArgs.length > 0">
|
||||
<h5 class="my-2">Arguments:</h5>
|
||||
<div class="border-divider border-l-2 pl-4">
|
||||
<div v-for="(field, index) in fieldArgs" :key="`field-${index}`">
|
||||
<span>
|
||||
{{ field.name }}:
|
||||
<GraphqlTypeLink
|
||||
:gql-type="field.type"
|
||||
:jump-type-callback="jumpTypeCallback"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
v-if="field.description"
|
||||
class="text-secondaryLight py-2 field-desc"
|
||||
>
|
||||
{{ field.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
gqlField: { type: Object, default: () => {} },
|
||||
jumpTypeCallback: { type: Function, default: () => {} },
|
||||
isHighlighted: { type: Boolean, default: false },
|
||||
},
|
||||
computed: {
|
||||
fieldName() {
|
||||
return this.gqlField.name
|
||||
},
|
||||
|
||||
fieldArgs() {
|
||||
return this.gqlField.args || []
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.field-highlighted {
|
||||
@apply border-b-2 border-accent;
|
||||
}
|
||||
|
||||
.field-title {
|
||||
@apply select-text;
|
||||
}
|
||||
</style>
|
||||
254
packages/hoppscotch-app/components/graphql/QueryEditor.vue
Normal file
254
packages/hoppscotch-app/components/graphql/QueryEditor.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<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>
|
||||
77
packages/hoppscotch-app/components/graphql/Request.vue
Normal file
77
packages/hoppscotch-app/components/graphql/Request.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="bg-primary flex p-4 top-0 z-10 sticky">
|
||||
<div class="space-x-2 flex-1 inline-flex">
|
||||
<input
|
||||
id="url"
|
||||
v-model="url"
|
||||
v-focus
|
||||
type="url"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
class="
|
||||
bg-primaryLight
|
||||
border border-divider
|
||||
rounded
|
||||
text-secondaryDark
|
||||
w-full
|
||||
py-2
|
||||
px-4
|
||||
hover:border-dividerDark
|
||||
focus-visible:bg-transparent focus-visible:border-dividerDark
|
||||
"
|
||||
:placeholder="$t('request.url')"
|
||||
@keyup.enter="onConnectClick"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
id="get"
|
||||
name="get"
|
||||
:label="!connected ? $t('action.connect') : $t('action.disconnect')"
|
||||
class="w-32"
|
||||
@click.native="onConnectClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "@nuxtjs/composition-api"
|
||||
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 url = useStream(gqlURL$, "", setGQLURL)
|
||||
|
||||
const onConnectClick = () => {
|
||||
if (!connected.value) {
|
||||
props.conn.connect(url.value, headers.value as any)
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-schema",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
} else {
|
||||
props.conn.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
connected,
|
||||
onConnectClick,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
555
packages/hoppscotch-app/components/graphql/RequestOptions.vue
Normal file
555
packages/hoppscotch-app/components/graphql/RequestOptions.vue
Normal file
@@ -0,0 +1,555 @@
|
||||
<template>
|
||||
<div>
|
||||
<SmartTabs styles="sticky bg-primary top-upperPrimaryStickyFold z-10">
|
||||
<SmartTab :id="'query'" :label="$t('tab.query')" :selected="true">
|
||||
<AppSection label="query">
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-upperSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
gqlRunQuery
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("request.query") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
:label="$t('request.run')"
|
||||
svg="play"
|
||||
class="rounded-none !text-accent"
|
||||
@click.native="runQuery()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io"
|
||||
blank
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copyQueryIcon"
|
||||
@click.native="copyQuery"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${$t(
|
||||
'action.prettify'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>P</kbd>`"
|
||||
:svg="prettifyQueryIcon"
|
||||
@click.native="prettifyQuery"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('request.save')"
|
||||
svg="folder-plus"
|
||||
@click.native="saveRequest"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'variables'" :label="$t('tab.variables')">
|
||||
<AppSection label="variables">
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-upperSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("request.variables") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io"
|
||||
blank
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copyVariablesIcon"
|
||||
@click.native="copyVariables"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'headers'" :label="$t('tab.headers')">
|
||||
<AppSection label="headers">
|
||||
<div
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
top-upperSecondaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("tab.headers") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io"
|
||||
blank
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.clear_all')"
|
||||
svg="trash-2"
|
||||
:disabled="bulkMode"
|
||||
@click.native="headers = []"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('state.bulk_mode')"
|
||||
svg="edit"
|
||||
:class="{ '!text-accent': bulkMode }"
|
||||
@click.native="bulkMode = !bulkMode"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('add.new')"
|
||||
svg="plus"
|
||||
:disabled="bulkMode"
|
||||
@click.native="addRequestHeader"
|
||||
/>
|
||||
</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-else>
|
||||
<div
|
||||
v-for="(header, index) in headers"
|
||||
:key="`header-${index}`"
|
||||
class="
|
||||
divide-x divide-dividerLight
|
||||
border-b border-dividerLight
|
||||
flex
|
||||
"
|
||||
>
|
||||
<SmartAutoComplete
|
||||
:placeholder="$t('count.header', { count: index + 1 })"
|
||||
:source="commonHeaders"
|
||||
:spellcheck="false"
|
||||
:value="header.key"
|
||||
autofocus
|
||||
styles="
|
||||
bg-transparent
|
||||
flex
|
||||
flex-1
|
||||
py-1
|
||||
px-4
|
||||
truncate
|
||||
focus:outline-none
|
||||
"
|
||||
@input="
|
||||
updateGQLHeader(index, {
|
||||
key: $event,
|
||||
value: header.value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<input
|
||||
class="bg-transparent flex flex-1 py-2 px-4"
|
||||
:placeholder="$t('count.value', { count: index + 1 })"
|
||||
:name="`value ${index}`"
|
||||
:value="header.value"
|
||||
autofocus
|
||||
@change="
|
||||
updateGQLHeader(index, {
|
||||
key: header.key,
|
||||
value: $event.target.value,
|
||||
active: header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
header.hasOwnProperty('active')
|
||||
? header.active
|
||||
? $t('action.turn_off')
|
||||
: $t('action.turn_on')
|
||||
: $t('action.turn_off')
|
||||
"
|
||||
:svg="
|
||||
header.hasOwnProperty('active')
|
||||
? header.active
|
||||
? 'check-circle'
|
||||
: 'circle'
|
||||
: 'check-circle'
|
||||
"
|
||||
color="green"
|
||||
@click.native="
|
||||
updateGQLHeader(index, {
|
||||
key: header.key,
|
||||
value: header.value,
|
||||
active: !header.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="removeRequestHeader(index)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="headers.length === 0"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<span class="text-center pb-4">
|
||||
{{ $t("empty.headers") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
:label="$t('add.new')"
|
||||
filled
|
||||
svg="plus"
|
||||
@click.native="addRequestHeader"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
|
||||
<CollectionsSaveRequest
|
||||
mode="graphql"
|
||||
:show="showSaveRequestModal"
|
||||
@hide-modal="hideRequestModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
onMounted,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import clone from "lodash/clone"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import {
|
||||
useNuxt,
|
||||
useReadonlyStream,
|
||||
useStream,
|
||||
} from "~/helpers/utils/composables"
|
||||
import {
|
||||
addGQLHeader,
|
||||
gqlHeaders$,
|
||||
gqlQuery$,
|
||||
gqlResponse$,
|
||||
gqlURL$,
|
||||
gqlVariables$,
|
||||
removeGQLHeader,
|
||||
setGQLHeaders,
|
||||
setGQLQuery,
|
||||
setGQLResponse,
|
||||
setGQLVariables,
|
||||
updateGQLHeader,
|
||||
} from "~/newstore/GQLSession"
|
||||
import { commonHeaders } from "~/helpers/headers"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { makeGQLRequest } from "~/helpers/types/HoppGQLRequest"
|
||||
|
||||
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 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 queryEditor = ref<any | null>(null)
|
||||
|
||||
const copyQueryIcon = ref("copy")
|
||||
const prettifyQueryIcon = ref("align-left")
|
||||
const copyVariablesIcon = ref("copy")
|
||||
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
const schema = useReadonlyStream(props.conn.schemaString$, "")
|
||||
|
||||
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,
|
||||
})
|
||||
)
|
||||
|
||||
$toast.success(t("state.finished_in", { duration }).toString(), {
|
||||
icon: "done",
|
||||
})
|
||||
} catch (e: any) {
|
||||
response.value = `${e}. ${t("error.check_console_details")}`
|
||||
nuxt.value.$loading.finish()
|
||||
|
||||
$toast.error(`${e} ${t("error.f12_details").toString()}`, {
|
||||
icon: "error_outline",
|
||||
})
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "graphql-query",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
}
|
||||
|
||||
const hideRequestModal = () => {
|
||||
showSaveRequestModal.value = false
|
||||
}
|
||||
|
||||
const prettifyQuery = () => {
|
||||
queryEditor.value.prettifyQuery()
|
||||
prettifyQueryIcon.value = "check"
|
||||
setTimeout(() => (prettifyQueryIcon.value = "align-left"), 1000)
|
||||
}
|
||||
|
||||
const saveRequest = () => {
|
||||
showSaveRequestModal.value = true
|
||||
}
|
||||
|
||||
// Why ?
|
||||
const updateQuery = (updatedQuery: string) => {
|
||||
gqlQueryString.value = updatedQuery
|
||||
}
|
||||
|
||||
const copyVariables = () => {
|
||||
copyToClipboard(variableString.value)
|
||||
copyVariablesIcon.value = "check"
|
||||
setTimeout(() => (copyVariablesIcon.value = "copy"), 1000)
|
||||
}
|
||||
|
||||
const addRequestHeader = () => {
|
||||
addGQLHeader({
|
||||
key: "",
|
||||
value: "",
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
|
||||
const removeRequestHeader = (index: number) => {
|
||||
removeGQLHeader(index)
|
||||
}
|
||||
|
||||
return {
|
||||
gqlQueryString,
|
||||
variableString,
|
||||
headers,
|
||||
copyQueryIcon,
|
||||
prettifyQueryIcon,
|
||||
copyVariablesIcon,
|
||||
|
||||
queryEditor,
|
||||
|
||||
showSaveRequestModal,
|
||||
hideRequestModal,
|
||||
|
||||
schema,
|
||||
|
||||
copyQuery,
|
||||
runQuery,
|
||||
prettifyQuery,
|
||||
saveRequest,
|
||||
updateQuery,
|
||||
copyVariables,
|
||||
addRequestHeader,
|
||||
removeRequestHeader,
|
||||
|
||||
getSpecialKey: getPlatformSpecialKey,
|
||||
|
||||
commonHeaders,
|
||||
updateGQLHeader,
|
||||
bulkMode,
|
||||
bulkHeaders,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
188
packages/hoppscotch-app/components/graphql/Response.vue
Normal file
188
packages/hoppscotch-app/components/graphql/Response.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<AppSection ref="response" label="response">
|
||||
<div
|
||||
v-if="responseString"
|
||||
class="
|
||||
bg-primary
|
||||
border-b border-dividerLight
|
||||
flex flex-1
|
||||
pl-4
|
||||
top-0
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("response.title") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
ref="downloadResponse"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.download_file')"
|
||||
:svg="downloadResponseIcon"
|
||||
@click.native="downloadResponse"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="copyResponseButton"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copyResponseIcon"
|
||||
@click.native="copyResponse"
|
||||
/>
|
||||
</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-else
|
||||
class="
|
||||
flex flex-col flex-1
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<div class="flex space-x-2 pb-4">
|
||||
<div class="flex flex-col space-y-4 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> -->
|
||||
</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>
|
||||
<ButtonSecondary
|
||||
:label="$t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io"
|
||||
svg="external-link"
|
||||
blank
|
||||
outline
|
||||
reverse
|
||||
/>
|
||||
</div>
|
||||
</AppSection>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
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,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
|
||||
const responseString = useReadonlyStream(gqlResponse$, "")
|
||||
|
||||
const downloadResponseIcon = ref("download")
|
||||
const copyResponseIcon = ref("copy")
|
||||
|
||||
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,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shortcut-key {
|
||||
@apply bg-dividerLight;
|
||||
@apply rounded;
|
||||
@apply ml-2;
|
||||
@apply py-1;
|
||||
@apply px-2;
|
||||
@apply inline-flex;
|
||||
}
|
||||
</style>
|
||||
470
packages/hoppscotch-app/components/graphql/Sidebar.vue
Normal file
470
packages/hoppscotch-app/components/graphql/Sidebar.vue
Normal file
@@ -0,0 +1,470 @@
|
||||
<template>
|
||||
<aside>
|
||||
<SmartTabs styles="sticky bg-primary z-10 top-0">
|
||||
<SmartTab :id="'docs'" :label="`Docs`" :selected="true">
|
||||
<AppSection label="docs">
|
||||
<div class="bg-primary flex top-sidebarPrimaryStickyFold z-10 sticky">
|
||||
<input
|
||||
v-model="graphqlFieldsFilterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
:placeholder="$t('action.search')"
|
||||
class="bg-transparent flex w-full p-4 py-2"
|
||||
/>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/quickstart/graphql"
|
||||
blank
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartTabs
|
||||
ref="gqlTabs"
|
||||
styles="border-t border-dividerLight bg-primary sticky z-10 top-sidebarSecondaryStickyFold"
|
||||
>
|
||||
<div class="gqlTabs">
|
||||
<SmartTab
|
||||
v-if="queryFields.length > 0"
|
||||
:id="'queries'"
|
||||
:label="$t('tab.queries')"
|
||||
:selected="true"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in filteredQueryFields"
|
||||
:key="`field-${index}`"
|
||||
:gql-field="field"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="mutationFields.length > 0"
|
||||
:id="'mutations'"
|
||||
:label="$t('graphql.mutations')"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in filteredMutationFields"
|
||||
:key="`field-${index}`"
|
||||
:gql-field="field"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="subscriptionFields.length > 0"
|
||||
:id="'subscriptions'"
|
||||
:label="$t('graphql.subscriptions')"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in filteredSubscriptionFields"
|
||||
:key="`field-${index}`"
|
||||
:gql-field="field"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
class="p-4"
|
||||
/>
|
||||
</SmartTab>
|
||||
<SmartTab
|
||||
v-if="graphqlTypes.length > 0"
|
||||
:id="'types'"
|
||||
ref="typesTab"
|
||||
:label="$t('tab.types')"
|
||||
class="divide-y divide-dividerLight"
|
||||
>
|
||||
<GraphqlType
|
||||
v-for="(type, index) in filteredGraphqlTypes"
|
||||
:key="`type-${index}`"
|
||||
:gql-type="type"
|
||||
:gql-types="graphqlTypes"
|
||||
:is-highlighted="isGqlTypeHighlighted(type)"
|
||||
:highlighted-fields="getGqlTypeHighlightedFields(type)"
|
||||
:jump-type-callback="handleJumpToType"
|
||||
/>
|
||||
</SmartTab>
|
||||
</div>
|
||||
</SmartTabs>
|
||||
<div
|
||||
v-if="
|
||||
queryFields.length === 0 &&
|
||||
mutationFields.length === 0 &&
|
||||
subscriptionFields.length === 0 &&
|
||||
graphqlTypes.length === 0
|
||||
"
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">link</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.schema") }}
|
||||
</span>
|
||||
</div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'history'" :label="$t('tab.history')">
|
||||
<History
|
||||
ref="graphqlHistoryComponent"
|
||||
:page="'graphql'"
|
||||
@useHistory="handleUseHistory"
|
||||
/>
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'collections'" :label="$t('tab.collections')">
|
||||
<CollectionsGraphql />
|
||||
</SmartTab>
|
||||
|
||||
<SmartTab :id="'schema'" :label="`Schema`">
|
||||
<AppSection ref="schema" label="schema">
|
||||
<div
|
||||
v-if="schemaString"
|
||||
class="
|
||||
bg-primary
|
||||
flex flex-1
|
||||
top-sidebarPrimaryStickyFold
|
||||
pl-4
|
||||
z-10
|
||||
sticky
|
||||
items-center
|
||||
justify-between
|
||||
"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("graphql.schema") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/quickstart/graphql"
|
||||
blank
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="downloadSchema"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.download_file')"
|
||||
:svg="downloadSchemaIcon"
|
||||
@click.native="downloadSchema"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
ref="copySchemaCode"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.copy')"
|
||||
:svg="copySchemaIcon"
|
||||
@click.native="copySchema"
|
||||
/>
|
||||
</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-else
|
||||
class="
|
||||
flex flex-col
|
||||
text-secondaryLight
|
||||
p-4
|
||||
items-center
|
||||
justify-center
|
||||
"
|
||||
>
|
||||
<i class="opacity-75 pb-2 material-icons">link</i>
|
||||
<span class="text-center">
|
||||
{{ $t("empty.schema") }}
|
||||
</span>
|
||||
</div>
|
||||
</AppSection>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
PropType,
|
||||
ref,
|
||||
useContext,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import { GraphQLField, GraphQLType } from "graphql"
|
||||
import { map } from "rxjs/operators"
|
||||
import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useReadonlyStream } from "~/helpers/utils/composables"
|
||||
import {
|
||||
setGQLHeaders,
|
||||
setGQLQuery,
|
||||
setGQLResponse,
|
||||
setGQLURL,
|
||||
setGQLVariables,
|
||||
} from "~/newstore/GQLSession"
|
||||
|
||||
function isTextFoundInGraphqlFieldObject(
|
||||
text: string,
|
||||
field: GraphQLField<any, any>
|
||||
) {
|
||||
const normalizedText = text.toLowerCase()
|
||||
|
||||
const isFilterTextFoundInDescription = field.description
|
||||
? field.description.toLowerCase().includes(normalizedText)
|
||||
: false
|
||||
const isFilterTextFoundInName = field.name
|
||||
.toLowerCase()
|
||||
.includes(normalizedText)
|
||||
|
||||
return isFilterTextFoundInDescription || isFilterTextFoundInName
|
||||
}
|
||||
|
||||
function getFilteredGraphqlFields(
|
||||
filterText: string,
|
||||
fields: GraphQLField<any, any>[]
|
||||
) {
|
||||
if (!filterText) return fields
|
||||
|
||||
return fields.filter((field) =>
|
||||
isTextFoundInGraphqlFieldObject(filterText, field)
|
||||
)
|
||||
}
|
||||
|
||||
function getFilteredGraphqlTypes(filterText: string, types: GraphQLType[]) {
|
||||
if (!filterText) return types
|
||||
|
||||
return types.filter((type) => {
|
||||
const isFilterTextMatching = isTextFoundInGraphqlFieldObject(
|
||||
filterText,
|
||||
type as any
|
||||
)
|
||||
|
||||
if (isFilterTextMatching) {
|
||||
return true
|
||||
}
|
||||
|
||||
const isFilterTextMatchingAtLeastOneField = Object.values(
|
||||
(type as any)._fields || {}
|
||||
).some((field) => isTextFoundInGraphqlFieldObject(filterText, field as any))
|
||||
|
||||
return isFilterTextMatchingAtLeastOneField
|
||||
})
|
||||
}
|
||||
|
||||
function resolveRootType(type: GraphQLType) {
|
||||
let t: any = type
|
||||
while (t.ofType) t = t.ofType
|
||||
return t
|
||||
}
|
||||
|
||||
type GQLHistoryEntry = {
|
||||
url: string
|
||||
headers: GQLHeader[]
|
||||
query: string
|
||||
response: string
|
||||
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 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 downloadSchemaIcon = ref("download")
|
||||
const copySchemaIcon = ref("copy")
|
||||
|
||||
const graphqlFieldsFilterText = ref("")
|
||||
|
||||
const gqlTabs = ref<any | null>(null)
|
||||
const typesTab = ref<any | null>(null)
|
||||
|
||||
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 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,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
107
packages/hoppscotch-app/components/graphql/Type.vue
Normal file
107
packages/hoppscotch-app/components/graphql/Type.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div :id="`type_${gqlType.name}`" class="p-4">
|
||||
<div class="type-title" :class="{ 'text-accent': isHighlighted }">
|
||||
<span v-if="isInput" class="text-accent">input </span>
|
||||
<span v-else-if="isInterface" class="text-accent">interface </span>
|
||||
<span v-else-if="isEnum" class="text-accent">enum </span>
|
||||
{{ gqlType.name }}
|
||||
</div>
|
||||
<div v-if="gqlType.description" class="text-secondaryLight py-2 type-desc">
|
||||
{{ gqlType.description }}
|
||||
</div>
|
||||
<div v-if="interfaces.length > 0">
|
||||
<h5 class="my-2">Interfaces:</h5>
|
||||
<div
|
||||
v-for="(gqlInterface, index) in interfaces"
|
||||
:key="`gqlInterface-${index}`"
|
||||
>
|
||||
<GraphqlTypeLink
|
||||
:gql-type="gqlInterface"
|
||||
:jump-type-callback="jumpTypeCallback"
|
||||
class="border-divider border-l-2 pl-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="children.length > 0" class="mb-2">
|
||||
<h5 class="my-2">Children:</h5>
|
||||
<GraphqlTypeLink
|
||||
v-for="(child, index) in children"
|
||||
:key="`child-${index}`"
|
||||
:gql-type="child"
|
||||
:jump-type-callback="jumpTypeCallback"
|
||||
class="border-divider border-l-2 pl-4"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="gqlType.getFields">
|
||||
<h5 class="my-2">Fields:</h5>
|
||||
<GraphqlField
|
||||
v-for="(field, index) in gqlType.getFields()"
|
||||
:key="`field-${index}`"
|
||||
class="border-divider border-l-2 pl-4"
|
||||
:gql-field="field"
|
||||
:is-highlighted="isFieldHighlighted({ field })"
|
||||
:jump-type-callback="jumpTypeCallback"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isEnum">
|
||||
<h5 class="my-2">Values:</h5>
|
||||
<div
|
||||
v-for="(value, index) in gqlType.getValues()"
|
||||
:key="`value-${index}`"
|
||||
class="border-divider border-l-2 pl-4"
|
||||
v-text="value.name"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import {
|
||||
GraphQLEnumType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInterfaceType,
|
||||
} from "graphql"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop, vue/require-prop-types
|
||||
gqlType: {},
|
||||
gqlTypes: { type: Array, default: () => [] },
|
||||
jumpTypeCallback: { type: Function, default: () => {} },
|
||||
isHighlighted: { type: Boolean, default: false },
|
||||
highlightedFields: { type: Array, default: () => [] },
|
||||
},
|
||||
computed: {
|
||||
isInput() {
|
||||
return this.gqlType instanceof GraphQLInputObjectType
|
||||
},
|
||||
isInterface() {
|
||||
return this.gqlType instanceof GraphQLInterfaceType
|
||||
},
|
||||
isEnum() {
|
||||
return this.gqlType instanceof GraphQLEnumType
|
||||
},
|
||||
interfaces() {
|
||||
return (this.gqlType.getInterfaces && this.gqlType.getInterfaces()) || []
|
||||
},
|
||||
children() {
|
||||
return this.gqlTypes.filter(
|
||||
(type) =>
|
||||
type.getInterfaces && type.getInterfaces().includes(this.gqlType)
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isFieldHighlighted({ field }) {
|
||||
return !!this.highlightedFields.find(({ name }) => name === field.name)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.type-highlighted {
|
||||
@apply text-accent;
|
||||
}
|
||||
</style>
|
||||
44
packages/hoppscotch-app/components/graphql/TypeLink.vue
Normal file
44
packages/hoppscotch-app/components/graphql/TypeLink.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<span
|
||||
:class="isScalar ? 'text-secondaryLight' : 'cursor-pointer text-accent'"
|
||||
@click="jumpToType"
|
||||
>
|
||||
{{ typeString }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import { GraphQLScalarType } from "graphql"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
gqlType: null,
|
||||
// (typeName: string) => void
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
jumpTypeCallback: Function,
|
||||
},
|
||||
|
||||
computed: {
|
||||
typeString() {
|
||||
return this.gqlType.toString()
|
||||
},
|
||||
isScalar() {
|
||||
return this.resolveRootType(this.gqlType) instanceof GraphQLScalarType
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
jumpToType() {
|
||||
if (this.isScalar) return
|
||||
this.jumpTypeCallback(this.gqlType)
|
||||
},
|
||||
resolveRootType(type) {
|
||||
let t = type
|
||||
while (t.ofType != null) t = t.ofType
|
||||
return t
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,169 @@
|
||||
import { shallowMount } from "@vue/test-utils"
|
||||
import field from "../Field"
|
||||
|
||||
const gqlField = {
|
||||
name: "testField",
|
||||
args: [
|
||||
{
|
||||
name: "arg1",
|
||||
type: "Arg1Type",
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
type: "Arg2type",
|
||||
},
|
||||
],
|
||||
type: "FieldType",
|
||||
description: "TestDescription",
|
||||
}
|
||||
|
||||
const factory = (props) =>
|
||||
shallowMount(field, {
|
||||
propsData: props,
|
||||
stubs: {
|
||||
GraphqlTypeLink: {
|
||||
template: "<span>Typelink</span>",
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$t: (text) => text,
|
||||
},
|
||||
})
|
||||
|
||||
describe("field", () => {
|
||||
test("mounts properly if props are given", () => {
|
||||
const wrapper = factory({
|
||||
gqlField,
|
||||
})
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
})
|
||||
|
||||
test("field title is set correctly for fields with no args", () => {
|
||||
const wrapper = factory({
|
||||
gqlField: {
|
||||
...gqlField,
|
||||
args: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(".field-title")
|
||||
.text()
|
||||
.replace(/[\r\n]+/g, "")
|
||||
.replace(/ +/g, " ")
|
||||
).toEqual("testField : Typelink")
|
||||
})
|
||||
|
||||
test("field title is correctly given for fields with single arg", () => {
|
||||
const wrapper = factory({
|
||||
gqlField: {
|
||||
...gqlField,
|
||||
args: [
|
||||
{
|
||||
name: "arg1",
|
||||
type: "Arg1Type",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(".field-title")
|
||||
.text()
|
||||
.replace(/[\r\n]+/g, "")
|
||||
.replace(/ +/g, " ")
|
||||
).toEqual("testField ( arg1: Typelink ) : Typelink")
|
||||
})
|
||||
|
||||
test("field title is correctly given for fields with multiple args", () => {
|
||||
const wrapper = factory({
|
||||
gqlField: {
|
||||
...gqlField,
|
||||
args: [
|
||||
{
|
||||
name: "arg1",
|
||||
type: "Arg1Type",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(".field-title")
|
||||
.text()
|
||||
.replace(/[\r\n]+/g, "")
|
||||
.replace(/ +/g, " ")
|
||||
).toEqual("testField ( arg1: Typelink ) : Typelink")
|
||||
})
|
||||
|
||||
test("all typelinks are passed the jump callback", () => {
|
||||
const wrapper = factory({
|
||||
gqlField: {
|
||||
...gqlField,
|
||||
args: [
|
||||
{
|
||||
name: "arg1",
|
||||
type: "Arg1Type",
|
||||
},
|
||||
{
|
||||
name: "arg2",
|
||||
type: "Arg2Type",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(".field-title")
|
||||
.text()
|
||||
.replace(/[\r\n]+/g, "")
|
||||
.replace(/ +/g, " ")
|
||||
).toEqual("testField ( arg1: Typelink , arg2: Typelink ) : Typelink")
|
||||
})
|
||||
|
||||
test("description is rendered when it is present", () => {
|
||||
const wrapper = factory({
|
||||
gqlField,
|
||||
})
|
||||
|
||||
expect(wrapper.find(".field-desc").text()).toEqual("TestDescription")
|
||||
})
|
||||
|
||||
test("description not rendered when it is not present", () => {
|
||||
const wrapper = factory({
|
||||
gqlField: {
|
||||
...gqlField,
|
||||
description: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find(".field-desc").exists()).toEqual(false)
|
||||
})
|
||||
|
||||
test("deprecation warning is displayed when field is deprecated", () => {
|
||||
const wrapper = factory({
|
||||
gqlField: {
|
||||
...gqlField,
|
||||
isDeprecated: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find(".field-deprecated").exists()).toEqual(true)
|
||||
})
|
||||
|
||||
test("deprecation warning is not displayed wwhen field is not deprecated", () => {
|
||||
const wrapper = factory({
|
||||
gqlField: {
|
||||
...gqlField,
|
||||
isDeprecated: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find(".field-deprecated").exists()).toEqual(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,241 @@
|
||||
import { shallowMount } from "@vue/test-utils"
|
||||
import {
|
||||
GraphQLEnumType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInterfaceType,
|
||||
GraphQLObjectType,
|
||||
} from "graphql"
|
||||
import type from "../Type"
|
||||
|
||||
const gqlType = {
|
||||
name: "TestType",
|
||||
description: "TestDescription",
|
||||
getFields: () => [{ name: "field1" }, { name: "field2" }],
|
||||
}
|
||||
|
||||
const factory = (props) =>
|
||||
shallowMount(type, {
|
||||
mocks: {
|
||||
$t: (text) => text,
|
||||
},
|
||||
propsData: { gqlTypes: [], ...props },
|
||||
stubs: ["GraphqlField", "GraphqlTypeLink"],
|
||||
})
|
||||
|
||||
describe("type", () => {
|
||||
test("mounts properly when props are passed", () => {
|
||||
const wrapper = factory({
|
||||
gqlType,
|
||||
})
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
})
|
||||
|
||||
test("title of the type is rendered properly", () => {
|
||||
const wrapper = factory({
|
||||
gqlType,
|
||||
})
|
||||
|
||||
expect(wrapper.find(".type-title").text()).toEqual("TestType")
|
||||
})
|
||||
|
||||
test("description of the type is rendered properly if present", () => {
|
||||
const wrapper = factory({
|
||||
gqlType,
|
||||
})
|
||||
|
||||
expect(wrapper.find(".type-desc").text()).toEqual("TestDescription")
|
||||
})
|
||||
|
||||
test("description of the type is not rendered if not present", () => {
|
||||
const wrapper = factory({
|
||||
gqlType: {
|
||||
...gqlType,
|
||||
description: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.find(".type-desc").exists()).toEqual(false)
|
||||
})
|
||||
|
||||
test("fields are not rendered if not present", () => {
|
||||
const wrapper = factory({
|
||||
gqlType: {
|
||||
...gqlType,
|
||||
getFields: undefined,
|
||||
},
|
||||
})
|
||||
expect(wrapper.find("GraphqlField-stub").exists()).toEqual(false)
|
||||
})
|
||||
|
||||
test("all fields are rendered if present with props passed properly", () => {
|
||||
const wrapper = factory({
|
||||
gqlType: {
|
||||
...gqlType,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.findAll("GraphqlField-stub").length).toEqual(2)
|
||||
})
|
||||
|
||||
test("prepends 'input' to type name for Input Types", () => {
|
||||
const testType = new GraphQLInputObjectType({
|
||||
name: "TestType",
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: testType,
|
||||
})
|
||||
|
||||
expect(wrapper.find(".type-title").text().startsWith("input")).toEqual(true)
|
||||
})
|
||||
|
||||
test("prepends 'interface' to type name for Interface Types", () => {
|
||||
const testType = new GraphQLInterfaceType({
|
||||
name: "TestType",
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: testType,
|
||||
})
|
||||
|
||||
expect(wrapper.find(".type-title").text().startsWith("interface")).toEqual(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
test("prepends 'enum' to type name for Enum Types", () => {
|
||||
const testType = new GraphQLEnumType({
|
||||
name: "TestType",
|
||||
values: {},
|
||||
})
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: testType,
|
||||
})
|
||||
|
||||
expect(wrapper.find(".type-title").text().startsWith("enum")).toEqual(true)
|
||||
})
|
||||
|
||||
test("'interfaces' computed property returns all the related interfaces", () => {
|
||||
const testInterfaceA = new GraphQLInterfaceType({
|
||||
name: "TestInterfaceA",
|
||||
fields: {},
|
||||
})
|
||||
const testInterfaceB = new GraphQLInterfaceType({
|
||||
name: "TestInterfaceB",
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const type = new GraphQLObjectType({
|
||||
name: "TestType",
|
||||
interfaces: [testInterfaceA, testInterfaceB],
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: type,
|
||||
})
|
||||
|
||||
expect(wrapper.vm.interfaces).toEqual(
|
||||
expect.arrayContaining([testInterfaceA, testInterfaceB])
|
||||
)
|
||||
})
|
||||
|
||||
test("'interfaces' computed property returns an empty array if there are no interfaces", () => {
|
||||
const type = new GraphQLObjectType({
|
||||
name: "TestType",
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: type,
|
||||
})
|
||||
|
||||
expect(wrapper.vm.interfaces).toEqual([])
|
||||
})
|
||||
|
||||
test("'interfaces' computed property returns an empty array if the type is an enum", () => {
|
||||
const type = new GraphQLEnumType({
|
||||
name: "TestType",
|
||||
values: {},
|
||||
})
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: type,
|
||||
})
|
||||
|
||||
expect(wrapper.vm.interfaces).toEqual([])
|
||||
})
|
||||
|
||||
test("'children' computed property returns all the types implementing an interface", () => {
|
||||
const testInterface = new GraphQLInterfaceType({
|
||||
name: "TestInterface",
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const typeA = new GraphQLObjectType({
|
||||
name: "TypeA",
|
||||
interfaces: [testInterface],
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const typeB = new GraphQLObjectType({
|
||||
name: "TypeB",
|
||||
interfaces: [testInterface],
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: testInterface,
|
||||
gqlTypes: [testInterface, typeA, typeB],
|
||||
})
|
||||
|
||||
expect(wrapper.vm.children).toEqual(expect.arrayContaining([typeA, typeB]))
|
||||
})
|
||||
|
||||
test("'children' computed property returns an empty array if there are no types implementing the interface", () => {
|
||||
const testInterface = new GraphQLInterfaceType({
|
||||
name: "TestInterface",
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const typeA = new GraphQLObjectType({
|
||||
name: "TypeA",
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const typeB = new GraphQLObjectType({
|
||||
name: "TypeB",
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: testInterface,
|
||||
gqlTypes: [testInterface, typeA, typeB],
|
||||
})
|
||||
|
||||
expect(wrapper.vm.children).toEqual([])
|
||||
})
|
||||
|
||||
test("'children' computed property returns an empty array if the type is an enum", () => {
|
||||
const testInterface = new GraphQLInterfaceType({
|
||||
name: "TestInterface",
|
||||
fields: {},
|
||||
})
|
||||
|
||||
const testType = new GraphQLEnumType({
|
||||
name: "TestEnum",
|
||||
values: {},
|
||||
})
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: testType,
|
||||
gqlTypes: [testInterface, testType],
|
||||
})
|
||||
|
||||
expect(wrapper.vm.children).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { shallowMount } from "@vue/test-utils"
|
||||
import { GraphQLInt } from "graphql"
|
||||
import typelink from "../TypeLink.vue"
|
||||
|
||||
const factory = (props) =>
|
||||
shallowMount(typelink, {
|
||||
propsData: props,
|
||||
})
|
||||
|
||||
const gqlType = {
|
||||
toString: () => "TestType",
|
||||
}
|
||||
|
||||
describe("typelink", () => {
|
||||
test("mounts properly when valid props are given", () => {
|
||||
const wrapper = factory({
|
||||
gqlType,
|
||||
jumpTypeCallback: jest.fn(),
|
||||
})
|
||||
|
||||
expect(wrapper).toBeTruthy()
|
||||
})
|
||||
|
||||
test("jumpToTypeCallback is called when the link is clicked", async () => {
|
||||
const callback = jest.fn()
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType,
|
||||
jumpTypeCallback: callback,
|
||||
})
|
||||
|
||||
await wrapper.trigger("click")
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("jumpToType callback is not called if the root type is a scalar", async () => {
|
||||
const callback = jest.fn()
|
||||
|
||||
const wrapper = factory({
|
||||
gqlType: GraphQLInt,
|
||||
jumpTypeCallback: callback,
|
||||
})
|
||||
|
||||
await wrapper.trigger("click")
|
||||
|
||||
expect(callback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("link text is the type string", () => {
|
||||
const wrapper = factory({
|
||||
gqlType,
|
||||
jumpTypeCallback: jest.fn(),
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toBe("TestType")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user