feat: migrate to vue 3 + vite (#2553)

Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
Co-authored-by: liyasthomas <liyascthomas@gmail.com>
This commit is contained in:
Andrew Bastin
2022-09-29 10:55:21 +05:30
committed by GitHub
parent 77a561b581
commit 8b300fab5d
685 changed files with 17102 additions and 25942 deletions

View File

@@ -0,0 +1,306 @@
<template>
<div class="flex flex-col flex-1">
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold text-secondaryLight">
{{ t("authorization.type") }}
</label>
<tippy
ref="authTypeOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<span class="select-wrapper">
<ButtonSecondary class="pr-8 ml-2 rounded-none" :label="authName" />
</span>
<template #content="{ hide }">
<div
class="flex flex-col space-y-1"
tabindex="0"
role="menu"
@keyup.escape="hide()"
>
<SmartItem
label="None"
:icon="authName === 'None' ? IconCircleDot : IconCircle"
:active="authName === 'None'"
@click="
() => {
authType = 'none'
hide()
}
"
/>
<SmartItem
label="Basic Auth"
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
:active="authName === 'Basic Auth'"
@click="
() => {
authType = 'basic'
hide()
}
"
/>
<SmartItem
label="Bearer Token"
:icon="authName === 'Bearer' ? IconCircleDot : IconCircle"
:active="authName === 'Bearer'"
@click="
() => {
authType = 'bearer'
hide()
}
"
/>
<SmartItem
label="OAuth 2.0"
:icon="authName === 'OAuth 2.0' ? IconCircleDot : IconCircle"
:active="authName === 'OAuth 2.0'"
@click="
() => {
authType = 'oauth-2'
hide()
}
"
/>
<SmartItem
label="API key"
:icon="authName === 'API key' ? IconCircleDot : IconCircle"
:active="authName === 'API key'"
@click="
() => {
authType = 'api-key'
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
<div class="flex">
<!-- <SmartCheckbox
:on="!URLExcludes.auth"
@change="setExclude('auth', !$event)"
>
{{ t("authorization.include_in_url") }}
</SmartCheckbox> -->
<SmartCheckbox
:on="authActive"
class="px-2"
@change="authActive = !authActive"
>
{{ t("state.enabled") }}
</SmartCheckbox>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/authorization"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear')"
:icon="IconTrash2"
@click="clearContent"
/>
</div>
</div>
<div
v-if="authType === 'none'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/login.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.authorization')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.authorization") }}
</span>
<ButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/features/authorization"
blank
:icon="IconExternalLink"
reverse
class="mb-4"
/>
</div>
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">
<div v-if="authType === 'basic'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="basicUsername"
:placeholder="t('authorization.username')"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="basicPassword"
:placeholder="t('authorization.password')"
/>
</div>
</div>
<div v-if="authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
</div>
</div>
<div v-if="authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="oauth2Token" placeholder="Token" />
</div>
<HttpOAuth2Authorization />
</div>
<div v-if="authType === 'api-key'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="apiKey" placeholder="Key" />
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="apiValue" placeholder="Value" />
</div>
<div class="flex items-center border-b border-dividerLight">
<span class="flex items-center">
<label class="ml-4 text-secondaryLight">
{{ t("authorization.pass_key_by") }}
</label>
<tippy
ref="addToOptions"
interactive
trigger="click"
theme="popover"
arrow
>
<span class="select-wrapper">
<ButtonSecondary
:label="addTo || t('state.none')"
class="pr-8 ml-2 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
class="flex flex-col"
tabindex="0"
role="menu"
@keyup.escape="hide()"
>
<SmartItem
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
:active="addTo === 'Headers'"
:label="'Headers'"
@click="
() => {
addTo = 'Headers'
hide()
}
"
/>
<SmartItem
:icon="
addTo === 'Query params' ? IconCircleDot : IconCircle
"
:active="addTo === 'Query params'"
:label="'Query params'"
@click="
() => {
addTo = 'Query params'
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
</div>
<div
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
>
<div class="pb-2 text-secondaryLight">
{{ t("helpers.authorization") }}
</div>
<SmartAnchor
class="link"
:label="t('authorization.learn')"
:icon="IconExternalLink"
to="https://docs.hoppscotch.io/features/authorization"
blank
reverse
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, Ref } from "vue"
import {
HoppGQLAuthAPIKey,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthOAuth2,
} from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { gqlAuth$, setGQLAuth } from "~/newstore/GQLSession"
import IconTrash2 from "~icons/lucide/trash-2"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle"
const t = useI18n()
const colorMode = useColorMode()
const auth = useStream(
gqlAuth$,
{ authType: "none", authActive: true },
setGQLAuth
)
const authType = pluckRef(auth, "authType")
const authName = computed(() => {
if (authType.value === "basic") return "Basic Auth"
else if (authType.value === "bearer") return "Bearer"
else if (authType.value === "oauth-2") return "OAuth 2.0"
else if (authType.value === "api-key") return "API key"
else return "None"
})
const authActive = pluckRef(auth, "authActive")
const basicUsername = pluckRef(auth as Ref<HoppGQLAuthBasic>, "username")
const basicPassword = pluckRef(auth as Ref<HoppGQLAuthBasic>, "password")
const bearerToken = pluckRef(auth as Ref<HoppGQLAuthBearer>, "token")
const oauth2Token = pluckRef(auth as Ref<HoppGQLAuthOAuth2>, "token")
const apiKey = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "key")
const apiValue = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "value")
const addTo = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "addTo")
if (typeof addTo.value === "undefined") {
addTo.value = "Headers"
apiKey.value = ""
apiValue.value = ""
}
const clearContent = () => {
auth.value = {
authType: "none",
authActive: true,
}
}
const authTypeOptions = ref<any | null>(null)
const addToOptions = ref<any | null>(null)
</script>

View File

@@ -0,0 +1,93 @@
<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="py-2 text-secondaryLight field-desc"
>
{{ gqlField.description }}
</div>
<div
v-if="gqlField.isDeprecated"
class="inline-block px-2 py-1 my-1 text-black bg-yellow-200 rounded field-deprecated"
>
{{ t("state.deprecated") }}
</div>
<div v-if="fieldArgs.length > 0">
<h5 class="my-2">Arguments:</h5>
<div class="pl-4 border-l-2 border-divider">
<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="py-2 text-secondaryLight field-desc"
>
{{ field.description }}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
// TypeScript + Script Setup this :)
import { defineComponent } from "vue"
import { useI18n } from "@composables/i18n"
export default defineComponent({
props: {
gqlField: { type: Object, default: () => ({}) },
jumpTypeCallback: { type: Function, default: () => ({}) },
isHighlighted: { type: Boolean, default: false },
},
setup() {
return {
t: useI18n(),
}
},
computed: {
fieldName() {
return this.gqlField.name
},
fieldArgs() {
return this.gqlField.args || []
},
},
})
</script>
<style lang="scss" scoped>
.field-highlighted {
@apply border-accent border-b-2;
}
.field-title {
@apply select-text;
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary"
>
<div class="inline-flex flex-1 space-x-2">
<input
id="url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
:placeholder="`${t('request.url')}`"
:disabled="connected"
@keyup.enter="onConnectClick"
/>
<ButtonPrimary
id="get"
name="get"
:label="!connected ? t('action.connect') : t('action.disconnect')"
class="w-32"
@click="onConnectClick"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { GQLConnection } from "~/helpers/GQLConnection"
import { getCurrentStrategyID } from "~/helpers/network"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
const t = useI18n()
const props = defineProps<{
conn: GQLConnection
}>()
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()
}
}
</script>

View File

@@ -0,0 +1,766 @@
<template>
<div class="flex flex-col flex-1 h-full">
<SmartTabs
v-model="selectedOptionTab"
styles="sticky bg-primary top-upperPrimaryStickyFold z-10"
render-inactive-tabs
>
<SmartTab
:id="'query'"
:label="`${t('tab.query')}`"
:indicator="gqlQueryString && gqlQueryString.length > 0 ? true : false"
>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold gqlRunQuery"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.query") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t(
'request.run'
)} <xmp>${getSpecialKey()}</xmp><xmp>G</xmp>`"
:label="`${t('request.run')}`"
:icon="IconPlay"
class="rounded-none !text-accent !hover:text-accentDark"
@click="runQuery()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t(
'request.save'
)} <xmp>${getSpecialKey()}</xmp><xmp>S</xmp>`"
:label="`${t('request.save')}`"
:icon="IconSave"
class="rounded-none"
@click="saveRequest"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/graphql"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLQuery()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyQueryIcon"
@click="prettifyQuery"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyQueryIcon"
@click="copyQuery"
/>
</div>
</div>
<div ref="queryEditor" class="flex flex-col flex-1"></div>
</SmartTab>
<SmartTab
:id="'variables'"
:label="`${t('tab.variables')}`"
:indicator="variableString && variableString.length > 0 ? true : false"
>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.variables") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/graphql"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLVariables()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyVariablesIcon"
@click="prettifyVariableString"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyVariablesIcon"
@click="copyVariables"
/>
</div>
</div>
<div ref="variableEditor" class="flex flex-col flex-1"></div>
</SmartTab>
<SmartTab
:id="'headers'"
:label="`${t('tab.headers')}`"
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
>
<div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold text-secondaryLight">
{{ t("tab.headers") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/graphql"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearContent()"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
:icon="IconEdit"
:class="{ '!text-accent': bulkMode }"
@click="bulkMode = !bulkMode"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
:disabled="bulkMode"
@click="addHeader"
/>
</div>
</div>
<div
v-if="bulkMode"
ref="bulkEditor"
class="flex flex-col flex-1"
></div>
<div v-else>
<draggable
v-model="workingHeaders"
:item-key="(header) => `header-${header.id}`"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<ButtonSecondary
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
content:
index !== workingHeaders?.length - 1
? t('action.drag_to_reorder')
: null,
}"
:icon="IconGripVertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingHeaders?.length - 1,
}"
tabindex="-1"
/>
</span>
<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
"
class="flex-1 !flex"
@input="
updateHeader(index, {
id: header.id,
key: $event,
value: header.value,
active: header.active,
})
"
/>
<input
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change="
updateHeader(index, {
id: header.id,
key: header.key,
value: ($event!.target! as HTMLInputElement).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')
"
:icon="
header.hasOwnProperty('active')
? header.active
? IconCheckCircle
: IconCircle
: IconCheckCircle
"
color="green"
@click="
updateHeader(index, {
id: header.id,
key: header.key,
value: header.value,
active: !header.active,
})
"
/>
</span>
<span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteHeader(index)"
/>
</span>
</div>
</template>
</draggable>
<div
v-if="workingHeaders.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/add_category.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.headers')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.headers") }}
</span>
<ButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
class="mb-4"
@click="addHeader"
/>
</div>
</div>
</SmartTab>
<SmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<GraphqlAuthorization />
</SmartTab>
</SmartTabs>
<CollectionsSaveRequest
mode="graphql"
:show="showSaveRequestModal"
@hide-modal="hideRequestModal"
/>
</div>
</template>
<script setup lang="ts">
import IconPlay from "~icons/lucide/play"
import IconSave from "~icons/lucide/save"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconPlus from "~icons/lucide/plus"
import IconGripVertical from "~icons/lucide/grip-vertical"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconTrash from "~icons/lucide/trash"
import IconCircle from "~icons/lucide/circle"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconInfo from "~icons/lucide/info"
import IconWand from "~icons/lucide/wand"
import { Ref, computed, reactive, ref, watch } from "vue"
import * as gql from "graphql"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import { pipe, flow } from "fp-ts/function"
import {
GQLHeader,
makeGQLRequest,
rawKeyValueEntriesToString,
parseRawKeyValueEntriesE,
RawKeyValueEntry,
} from "@hoppscotch/data"
import draggable from "vuedraggable"
import { clone, cloneDeep, isEqual } from "lodash-es"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
import {
gqlAuth$,
gqlHeaders$,
gqlQuery$,
gqlResponse$,
gqlURL$,
gqlVariables$,
setGQLAuth,
setGQLHeaders,
setGQLQuery,
setGQLResponse,
setGQLVariables,
} 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 { useCodemirror } from "@composables/codemirror"
import jsonLinter from "~/helpers/editor/linting/json"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { objRemoveKey } from "~/helpers/functional/object"
type OptionTabs = "query" | "headers" | "variables" | "authorization"
const colorMode = useColorMode()
const selectedOptionTab = ref<OptionTabs>("query")
const t = useI18n()
const props = defineProps<{
conn: GQLConnection
}>()
const toast = useToast()
const url = useReadonlyStream(gqlURL$, "")
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery)
const variableString = useStream(gqlVariables$, "", setGQLVariables)
const idTicker = ref(0)
const bulkMode = ref(false)
const bulkHeaders = ref("")
const bulkEditor = ref<any | null>(null)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(bulkEditor, bulkHeaders, {
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
},
linter: null,
completer: null,
environmentHighlights: false,
})
// The functional headers list (the headers actually in the system)
const headers = useStream(gqlHeaders$, [], setGQLHeaders) as Ref<GQLHeader[]>
const auth = useStream(
gqlAuth$,
{ authType: "none", authActive: true },
setGQLAuth
)
// The UI representation of the headers list (has the empty end header)
const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
])
// Rule: Working Headers always have one empty header or the last element is always an empty header
watch(workingHeaders, (headersList) => {
if (
headersList.length > 0 &&
headersList[headersList.length - 1].key !== ""
) {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
// Sync logic between headers and working headers
watch(
headers,
(newHeadersList) => {
// Sync should overwrite working headers
const filteredWorkingHeaders = pipe(
workingHeaders.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(bulkHeaders.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
workingHeaders.value = pipe(
newHeadersList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
}
},
{ immediate: true }
)
watch(workingHeaders, (newWorkingHeaders) => {
const fixedHeaders = pipe(
newWorkingHeaders,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(headers.value, fixedHeaders)) {
headers.value = cloneDeep(fixedHeaders)
}
})
// Bulk Editor Syncing with Working Headers
watch(bulkHeaders, (newBulkHeaders) => {
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(newBulkHeaders),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(headers.value, filteredBulkHeaders)) {
headers.value = filteredBulkHeaders
}
})
watch(workingHeaders, (newHeadersList) => {
// If we are in bulk mode, don't apply direct changes
if (bulkMode.value) return
try {
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(filteredHeaders)
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const addHeader = () => {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateHeader = (index: number, header: GQLHeader & { id: number }) => {
workingHeaders.value = workingHeaders.value.map((h, i) =>
i === index ? header : h
)
}
const deleteHeader = (index: number) => {
const headersBeforeDeletion = clone(workingHeaders.value)
if (
!(
headersBeforeDeletion.length > 0 &&
index === headersBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingHeaders.value = headersBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingHeaders.value.splice(index, 1)
}
const clearContent = () => {
// set headers list to the initial state
workingHeaders.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkHeaders.value = ""
}
const activeGQLHeadersCount = computed(
() =>
headers.value.filter((x) => x.active && (x.key !== "" || x.value !== ""))
.length
)
const variableEditor = ref<any | null>(null)
useCodemirror(
variableEditor,
variableString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
placeholder: `${t("request.variables")}`,
},
linter: computed(() =>
variableString.value.length > 0 ? jsonLinter : null
),
completer: null,
environmentHighlights: false,
})
)
const queryEditor = ref<any | null>(null)
const schema = useReadonlyStream(props.conn.schema$, null, "noclone")
useCodemirror(queryEditor, gqlQueryString, {
extendedEditorConfig: {
mode: "graphql",
placeholder: `${t("request.query")}`,
},
linter: createGQLQueryLinter(schema),
completer: queryCompleter(schema),
environmentHighlights: false,
})
const copyQueryIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const prettifyQueryIcon = refAutoReset<
typeof IconWand | typeof IconCheck | typeof IconInfo
>(IconWand, 1000)
const prettifyVariablesIcon = refAutoReset<
typeof IconWand | typeof IconCheck | typeof IconInfo
>(IconWand, 1000)
const showSaveRequestModal = ref(false)
const copyQuery = () => {
copyToClipboard(gqlQueryString.value)
copyQueryIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const response = useStream(gqlResponse$, "", setGQLResponse)
const runQuery = async () => {
const startTime = Date.now()
startPageProgress()
response.value = "loading"
try {
const runURL = clone(url.value)
const runHeaders = clone(headers.value)
const runQuery = clone(gqlQueryString.value)
const runVariables = clone(variableString.value)
const runAuth = clone(auth.value)
const responseText = await props.conn.runQuery(
runURL,
runHeaders,
runQuery,
runVariables,
runAuth
)
const duration = Date.now() - startTime
completePageProgress()
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
addGraphqlHistoryEntry(
makeGQLHistoryEntry({
request: makeGQLRequest({
name: "",
url: runURL,
query: runQuery,
headers: runHeaders,
variables: runVariables,
auth: runAuth,
}),
response: response.value,
star: false,
})
)
toast.success(`${t("state.finished_in", { duration })}`)
} catch (e: any) {
response.value = `${e}`
completePageProgress()
toast.error(
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
{}
)
console.error(e)
}
logHoppRequestRunToAnalytics({
platform: "graphql-query",
strategy: getCurrentStrategyID(),
})
}
const hideRequestModal = () => {
showSaveRequestModal.value = false
}
const prettifyQuery = () => {
try {
gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
prettifyQueryIcon.value = IconCheck
} catch (e) {
toast.error(`${t("error.gql_prettify_invalid_query")}`)
prettifyQueryIcon.value = IconInfo
}
}
const saveRequest = () => {
showSaveRequestModal.value = true
}
const copyVariables = () => {
copyToClipboard(variableString.value)
copyVariablesIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const prettifyVariableString = () => {
try {
const jsonObj = JSON.parse(variableString.value)
variableString.value = JSON.stringify(jsonObj, null, 2)
prettifyVariablesIcon.value = IconCheck
} catch (e) {
console.error(e)
prettifyVariablesIcon.value = IconInfo
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
}
const clearGQLQuery = () => {
gqlQueryString.value = ""
}
const clearGQLVariables = () => {
variableString.value = ""
}
defineActionHandler("request.send-cancel", runQuery)
defineActionHandler("request.save", saveRequest)
defineActionHandler("request.reset", clearGQLQuery)
</script>

View File

@@ -0,0 +1,144 @@
<template>
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
<div
v-if="responseString === 'loading'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<SmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-else-if="responseString" class="flex flex-col flex-1">
<div
class="sticky top-0 z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
>
<label class="font-semibold text-secondaryLight">
{{ t("response.title") }}
</label>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:icon="downloadResponseIcon"
@click="downloadResponse"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyResponseIcon"
@click="copyResponse"
/>
</div>
</div>
<div ref="schemaEditor" class="flex flex-col flex-1"></div>
</div>
<div
v-else
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<div class="flex pb-4 my-4 space-x-2">
<div class="flex flex-col items-end text-right space-y-4">
<span class="flex items-center flex-1">
{{ t("shortcut.general.command_menu") }}
</span>
<span class="flex items-center flex-1">
{{ t("shortcut.general.help_menu") }}
</span>
</div>
<div class="flex flex-col space-y-4">
<div class="flex">
<kbd class="shortcut-key">/</kbd>
</div>
<div class="flex">
<kbd class="shortcut-key">?</kbd>
</div>
</div>
</div>
<ButtonSecondary
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io/features/response"
:icon="IconExternalLink"
blank
outline
reverse
/>
</div>
</div>
</template>
<script setup lang="ts">
import IconWrapText from "~icons/lucide/wrap-text"
import IconExternalLink from "~icons/lucide/external-link"
import IconDownload from "~icons/lucide/download"
import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy"
import { reactive, ref } from "vue"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { gqlResponse$ } from "~/newstore/GQLSession"
const t = useI18n()
const toast = useToast()
const responseString = useReadonlyStream(gqlResponse$, "")
const schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
schemaEditor,
responseString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
readOnly: true,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
const downloadResponseIcon = refAutoReset<
typeof IconDownload | typeof IconCheck
>(IconDownload, 1000)
const copyResponseIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const copyResponse = () => {
copyToClipboard(responseString.value!)
copyResponseIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
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 = IconCheck
toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
}
</script>

View File

@@ -0,0 +1,466 @@
<template>
<SmartTabs
v-model="selectedNavigationTab"
styles="sticky bg-primary z-10 top-0"
vertical
render-inactive-tabs
>
<SmartTab :id="'history'" :icon="IconClock" :label="`${t('tab.history')}`">
<History :page="'graphql'" @use-history="handleUseHistory" />
</SmartTab>
<SmartTab
:id="'collections'"
:icon="IconFolder"
:label="`${t('tab.collections')}`"
>
<CollectionsGraphql />
</SmartTab>
<SmartTab
:id="'docs'"
:icon="IconBookOpen"
:label="`${t('tab.documentation')}`"
>
<div
v-if="
queryFields.length === 0 &&
mutationFields.length === 0 &&
subscriptionFields.length === 0 &&
graphqlTypes.length === 0
"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/add_comment.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.documentation')}`"
/>
<span class="mb-4 text-center">
{{ t("empty.documentation") }}
</span>
</div>
<div v-else>
<div class="sticky top-0 z-10 flex bg-primary">
<input
v-model="graphqlFieldsFilterText"
type="search"
autocomplete="off"
:placeholder="`${t('action.search')}`"
class="flex flex-1 p-4 py-2 bg-transparent"
/>
<div class="flex">
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/quickstart/graphql"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
</div>
</div>
<SmartTabs
v-model="selectedGqlTab"
styles="border-t border-b border-dividerLight bg-primary sticky z-10 top-sidebarPrimaryStickyFold"
render-inactive-tabs
>
<SmartTab
v-if="queryFields.length > 0"
:id="'queries'"
:label="`${t('tab.queries')}`"
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'"
: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>
</SmartTabs>
</div>
</SmartTab>
<SmartTab :id="'schema'" :icon="IconBox" :label="`${t('tab.schema')}`">
<div
v-if="schemaString"
class="sticky top-0 z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
>
<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')"
:icon="IconHelpCircle"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:icon="downloadSchemaIcon"
@click="downloadSchema"
/>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copySchemaIcon"
@click="copySchema"
/>
</div>
</div>
<div
v-if="schemaString"
ref="schemaEditor"
class="flex flex-col flex-1"
></div>
<div
v-else
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.schema')}`"
/>
<span class="mb-4 text-center">
{{ t("empty.schema") }}
</span>
</div>
</SmartTab>
</SmartTabs>
</template>
<script setup lang="ts">
import IconFolder from "~icons/lucide/folder"
import IconBookOpen from "~icons/lucide/book-open"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconWrapText from "~icons/lucide/wrap-text"
import IconDownload from "~icons/lucide/download"
import IconCheck from "~icons/lucide/check"
import IconClock from "~icons/lucide/clock"
import IconCopy from "~icons/lucide/copy"
import IconBox from "~icons/lucide/box"
import { computed, nextTick, reactive, ref } from "vue"
import { GraphQLField, GraphQLType } from "graphql"
import { map } from "rxjs/operators"
import { GQLHeader } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror"
import { GQLConnection } from "@helpers/GQLConnection"
import { copyToClipboard } from "@helpers/utils/clipboard"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming"
import {
setGQLAuth,
setGQLHeaders,
setGQLQuery,
setGQLResponse,
setGQLURL,
setGQLVariables,
} from "~/newstore/GQLSession"
type NavigationTabs = "history" | "collection" | "docs" | "schema"
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
const selectedNavigationTab = ref<NavigationTabs>("history")
const selectedGqlTab = ref<GqlTabs>("queries")
const t = useI18n()
const colorMode = useColorMode()
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
}
const props = defineProps<{
conn: GQLConnection
}>()
const toast = useToast()
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 = refAutoReset<typeof IconDownload | typeof IconCheck>(
IconDownload,
1000
)
const copySchemaIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const graphqlFieldsFilterText = ref("")
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) => {
selectedGqlTab.value = "types"
await nextTick()
const rootTypeName = resolveRootType(type).name
const target = document.getElementById(`type_${rootTypeName}`)
if (target) {
target.scrollIntoView({ block: "center", behavior: "smooth" })
target.classList.add(
"transition-all",
"ring-inset",
"ring-accentLight",
"ring-4"
)
setTimeout(
() =>
target.classList.remove(
"ring-inset",
"ring-accentLight",
"ring-4",
"transition-all"
),
2000
)
}
}
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,
environmentHighlights: false,
})
)
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 = IconCheck
toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
}
const copySchema = () => {
if (!schemaString.value) return
copyToClipboard(schemaString.value)
copySchemaIcon.value = IconCheck
}
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)
setGQLAuth({
authType: "none",
authActive: true,
})
props.conn.reset()
}
</script>

View File

@@ -0,0 +1,109 @@
<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="py-2 text-secondaryLight 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="pl-4 border-l-2 border-divider"
/>
</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="pl-4 border-l-2 border-divider"
/>
</div>
<div v-if="gqlType.getFields">
<h5 class="my-2">Fields:</h5>
<GraphqlField
v-for="(field, index) in gqlType.getFields()"
:key="`field-${index}`"
class="pl-4 border-l-2 border-divider"
: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="pl-4 border-l-2 border-divider"
v-text="value.name"
></div>
</div>
</div>
</template>
<script>
// TODO: TypeScript + Setup Script this at some point :)
import { defineComponent } from "vue"
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 lang="scss" scoped>
.type-highlighted {
@apply text-accent;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<span
:class="isScalar ? 'text-secondaryLight' : 'cursor-pointer text-accent'"
@click="jumpToType"
>
{{ typeString }}
</span>
</template>
<script lang="ts">
import { defineComponent } from "vue"
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}`
},
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>