feat: new ui for response interface generation (#4105)
* feat: codegen body added * feat: new ui added for response interface * feat: generate code component added * chore: default collection tab * feat: generate data schema * chore: clean up * chore: minor code refactor * fix: only render if `isDrawerOpen` is true * chore: clean up * chore: clean up --------- Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||
<FirebaseLogin v-if="showLogin" @hide-modal="showLogin = false" />
|
||||
<HttpResponseInterface
|
||||
v-if="isDrawerOpen"
|
||||
:show="isDrawerOpen"
|
||||
@close="isDrawerOpen = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -11,6 +16,7 @@ import { defineActionHandler } from "~/helpers/actions"
|
||||
const showShortcuts = ref(false)
|
||||
const showShare = ref(false)
|
||||
const showLogin = ref(false)
|
||||
const isDrawerOpen = ref(false)
|
||||
|
||||
defineActionHandler("flyouts.keybinds.toggle", () => {
|
||||
showShortcuts.value = !showShortcuts.value
|
||||
@@ -23,4 +29,8 @@ defineActionHandler("modals.share.toggle", () => {
|
||||
defineActionHandler("modals.login.toggle", () => {
|
||||
showLogin.value = !showLogin.value
|
||||
})
|
||||
|
||||
defineActionHandler("response.schema.toggle", () => {
|
||||
isDrawerOpen.value = !isDrawerOpen.value
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
>
|
||||
<Splitpanes
|
||||
class="smart-splitter"
|
||||
:horizontal="COLUMN_LAYOUT"
|
||||
:horizontal="COLUMN_LAYOUT || forceColumnLayout"
|
||||
@resize="setPaneEvent($event, 'horizontal')"
|
||||
>
|
||||
<Pane
|
||||
@@ -79,6 +79,10 @@ const props = defineProps({
|
||||
default: null,
|
||||
},
|
||||
isEmbed: {
|
||||
type: Boolean,
|
||||
defaul: false,
|
||||
},
|
||||
forceColumnLayout: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<HoppSmartSlideOver
|
||||
:show="show"
|
||||
:title="t('response.data_schema')"
|
||||
@close="close()"
|
||||
>
|
||||
<template #content>
|
||||
<div class="flex flex-col px-4 flex-1 overflow-y-auto">
|
||||
<div class="flex flex-col">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
placement="bottom"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
class="mt-4"
|
||||
>
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
:label="selectedInterface"
|
||||
outline
|
||||
class="flex-1 pr-8"
|
||||
/>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="sticky top-0 z-10 flex-shrink-0 overflow-x-auto">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="input flex w-full !bg-primaryContrast p-4 py-2"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="lang in filteredResponseInterfaces"
|
||||
:key="lang"
|
||||
:label="lang"
|
||||
:info-icon="
|
||||
lang === selectedInterface ? IconCheck : undefined
|
||||
"
|
||||
:active-info-icon="lang === selectedInterface"
|
||||
@click="
|
||||
() => {
|
||||
selectedInterface = lang
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="filteredResponseInterfaces.length === 0"
|
||||
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="svg-icons opacity-75" />
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</div>
|
||||
<div
|
||||
v-if="errorState"
|
||||
class="my-4 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 font-mono text-red-400"
|
||||
>
|
||||
{{ t("error.something_went_wrong") }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="selectedInterface"
|
||||
class="my-4 rounded border border-dividerLight flex-1 overflow-hidden flex flex-col"
|
||||
>
|
||||
<div class="flex items-center justify-between pl-4">
|
||||
<label class="truncate font-semibold text-secondaryLight">
|
||||
{{ t("request.generated_code") }}
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': WRAP_LINES }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="toggleNestedSetting('WRAP_LINES', 'codeGen')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="downloadIcon"
|
||||
@click="downloadResponse"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@click="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="h-full relative w-full flex flex-col flex-1 rounded-b border-t border-dividerLight"
|
||||
>
|
||||
<div ref="generatedCode" class="absolute inset-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartSlideOver>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { computed, reactive, ref } from "vue"
|
||||
import {
|
||||
getResponseBodyText,
|
||||
useCopyResponse,
|
||||
useDownloadResponse,
|
||||
} from "~/composables/lens-actions"
|
||||
|
||||
import { useService } from "dioc/vue"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import jsonToLanguage from "~/helpers/utils/json-to-language"
|
||||
import { watch } from "vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void
|
||||
}>()
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
const selectedInterface = ref<(typeof interfaceLanguages)[number]>("TypeScript")
|
||||
const response = computed(() => {
|
||||
const res = tabs.currentActiveTab.value.document.response
|
||||
if (res?.type === "success" || res?.type === "fail") {
|
||||
return getResponseBodyText(res.body)
|
||||
}
|
||||
return ""
|
||||
})
|
||||
const errorState = ref(false)
|
||||
|
||||
const interfaceCode = ref("")
|
||||
|
||||
const setInterfaceCode = async () => {
|
||||
const res = await jsonToLanguage(selectedInterface.value, response.value)
|
||||
interfaceCode.value = res.lines.join("\n")
|
||||
}
|
||||
|
||||
watch(selectedInterface, setInterfaceCode)
|
||||
watch(response, setInterfaceCode, { immediate: true })
|
||||
|
||||
const close = () => {
|
||||
emit("close")
|
||||
}
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const generatedCode = ref<any | null>(null)
|
||||
const WRAP_LINES = useNestedSetting("WRAP_LINES", "codeGen")
|
||||
|
||||
useCodemirror(
|
||||
generatedCode,
|
||||
interfaceCode,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
readOnly: true,
|
||||
lineWrapping: WRAP_LINES,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
})
|
||||
)
|
||||
|
||||
const searchQuery = ref("")
|
||||
|
||||
const filteredResponseInterfaces = computed(() => {
|
||||
return interfaceLanguages.filter((lang) =>
|
||||
lang.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
const { copyIcon, copyResponse } = useCopyResponse(interfaceCode)
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
"",
|
||||
interfaceCode
|
||||
)
|
||||
</script>
|
||||
@@ -53,7 +53,7 @@
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('app.copy_interface_type')"
|
||||
:title="t('action.more')"
|
||||
:icon="IconMore"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
@@ -64,15 +64,14 @@
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="(language, index) in interfaceLanguages"
|
||||
:key="index"
|
||||
:label="language"
|
||||
:icon="
|
||||
copiedInterfaceLanguage === language
|
||||
? copyInterfaceIcon
|
||||
: IconCopy
|
||||
:label="t('response.generate_data_schema')"
|
||||
:icon="IconNetwork"
|
||||
@click="
|
||||
() => {
|
||||
invokeAction('response.schema.toggle')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
@click="runCopyInterface(language)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -234,7 +233,7 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconFilter from "~icons/lucide/filter"
|
||||
import IconMore from "~icons/lucide/more-horizontal"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconNetwork from "~icons/lucide/network"
|
||||
import * as LJSON from "lossless-json"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as E from "fp-ts/Either"
|
||||
@@ -254,13 +253,11 @@ import {
|
||||
useCopyResponse,
|
||||
useResponseBody,
|
||||
useDownloadResponse,
|
||||
useCopyInterface,
|
||||
} from "@composables/lens-actions"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -272,13 +269,6 @@ const { responseBodyText } = useResponseBody(props.response)
|
||||
|
||||
const toggleFilter = ref(false)
|
||||
const filterQueryText = ref("")
|
||||
const copiedInterfaceLanguage = ref("")
|
||||
|
||||
const runCopyInterface = (language: string) => {
|
||||
copyInterface(language).then(() => {
|
||||
copiedInterfaceLanguage.value = language
|
||||
})
|
||||
}
|
||||
|
||||
type BodyParseError =
|
||||
| { type: "JSON_PARSE_FAILED" }
|
||||
@@ -362,7 +352,6 @@ const filterResponseError = computed(() =>
|
||||
)
|
||||
|
||||
const { copyIcon, copyResponse } = useCopyResponse(jsonBodyText)
|
||||
const { copyInterfaceIcon, copyInterface } = useCopyInterface(jsonBodyText)
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
"application/json",
|
||||
jsonBodyText
|
||||
|
||||
Reference in New Issue
Block a user