123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- <template>
- <SmartTabs styles="sticky bg-primary z-10 top-0" vertical>
- <SmartTab
- :id="'history'"
- icon="clock"
- :label="`${t('tab.history')}`"
- :selected="true"
- >
- <History
- ref="graphqlHistoryComponent"
- :page="'graphql'"
- @useHistory="handleUseHistory"
- />
- </SmartTab>
- <SmartTab
- :id="'collections'"
- icon="folder"
- :label="`${t('tab.collections')}`"
- >
- <CollectionsGraphql />
- </SmartTab>
- <SmartTab
- :id="'docs'"
- icon="book-open"
- :label="`${t('tab.documentation')}`"
- >
- <AppSection label="docs">
- <div
- v-if="
- queryFields.length === 0 &&
- mutationFields.length === 0 &&
- subscriptionFields.length === 0 &&
- graphqlTypes.length === 0
- "
- class="text-secondaryLight flex flex-col items-center justify-center p-4"
- >
- <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="bg-primary sticky top-0 z-10 flex">
- <input
- v-model="graphqlFieldsFilterText"
- type="search"
- autocomplete="off"
- :placeholder="`${t('action.search')}`"
- class="flex w-full 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')"
- svg="help-circle"
- />
- </div>
- </div>
- <SmartTabs
- ref="gqlTabs"
- styles="border-t border-b border-dividerLight bg-primary sticky z-10 top-sidebarPrimaryStickyFold"
- >
- <div class="gqlTabs">
- <SmartTab
- v-if="queryFields.length > 0"
- :id="'queries'"
- :label="`${t('tab.queries')}`"
- :selected="true"
- class="divide-dividerLight divide-y"
- >
- <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-dividerLight divide-y"
- >
- <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-dividerLight divide-y"
- >
- <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-dividerLight divide-y"
- >
- <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>
- </AppSection>
- </SmartTab>
- <SmartTab :id="'schema'" icon="box" :label="`${t('tab.schema')}`">
- <AppSection ref="schema" label="schema">
- <div
- v-if="schemaString"
- class="bg-primary border-dividerLight sticky top-0 z-10 flex items-center justify-between flex-1 pl-4 border-b"
- >
- <label class="text-secondaryLight font-semibold">
- {{ 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
- v-tippy="{ theme: 'tooltip' }"
- :title="t('state.linewrap')"
- :class="{ '!text-accent': linewrapEnabled }"
- svg="corner-down-left"
- @click.native.prevent="linewrapEnabled = !linewrapEnabled"
- />
- <ButtonSecondary
- ref="downloadSchema"
- v-tippy="{ theme: 'tooltip' }"
- :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>
- <div v-if="schemaString" ref="schemaEditor"></div>
- <div
- v-else
- class="text-secondaryLight flex flex-col items-center justify-center p-4"
- >
- <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>
- </AppSection>
- </SmartTab>
- </SmartTabs>
- </template>
- <script setup lang="ts">
- import { computed, nextTick, reactive, ref } from "@nuxtjs/composition-api"
- import { GraphQLField, GraphQLType } from "graphql"
- import { map } from "rxjs/operators"
- import { useCodemirror } from "~/helpers/editor/codemirror"
- import { GQLConnection } from "~/helpers/GQLConnection"
- import { GQLHeader } from "~/helpers/types/HoppGQLRequest"
- import { copyToClipboard } from "~/helpers/utils/clipboard"
- import {
- useReadonlyStream,
- useI18n,
- useToast,
- } from "~/helpers/utils/composables"
- import {
- setGQLHeaders,
- setGQLQuery,
- setGQLResponse,
- setGQLURL,
- setGQLVariables,
- } from "~/newstore/GQLSession"
- const t = useI18n()
- 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 = 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) {
- 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,
- })
- )
- 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")}`)
- setTimeout(() => {
- document.body.removeChild(a)
- URL.revokeObjectURL(url)
- downloadSchemaIcon.value = "download"
- }, 1000)
- }
- const copySchema = () => {
- if (!schemaString.value) return
- copyToClipboard(schemaString.value)
- copySchemaIcon.value = "check"
- setTimeout(() => (copySchemaIcon.value = "copy"), 1000)
- }
- const handleUseHistory = (entry: GQLHistoryEntry) => {
- const url = entry.url
- const headers = entry.headers
- const gqlQueryString = entry.query
- const variableString = entry.variables
- const responseText = entry.response
- setGQLURL(url)
- setGQLHeaders(headers)
- setGQLQuery(gqlQueryString)
- setGQLVariables(variableString)
- setGQLResponse(responseText)
- props.conn.reset()
- }
- </script>
|