Compare commits

...

19 Commits

Author SHA1 Message Date
nivedin
4aaf5c279c fix: show response section only if response is present 2024-01-23 22:24:01 +05:30
nivedin
dcb8b2c511 refactor: update loading state UI in embed 2024-01-23 22:24:01 +05:30
nivedin
58d3ef900f refactor: minor code update 2024-01-23 22:24:01 +05:30
nivedin
0a94c169bd fix: no selected option tab during the initial load 2024-01-23 22:24:01 +05:30
nivedin
80eb4c7701 fix: shortcode subscription failing to initialize 2024-01-23 22:24:01 +05:30
nivedin
6ed4b73a34 chore: code refactor 2024-01-23 22:24:01 +05:30
nivedin
52ef2d8d32 fix: remove redundant array creation 2024-01-23 22:24:01 +05:30
nivedin
53e013e2a4 chore: minor padding update 2024-01-23 22:24:01 +05:30
nivedin
4a14de76c9 chore: minor code update 2024-01-23 22:24:01 +05:30
nivedin
a4aa808103 chore: minor update for team header-auth properties 2024-01-23 22:24:01 +05:30
nivedin
6d7b0e11a3 fix: add loading and error state 2024-01-23 22:24:01 +05:30
nivedin
ffff54b5af fix: update i18n text 2024-01-23 22:24:01 +05:30
Anwarul Islam
b81ccb4ee3 fix: tab on current input field to focus the next input field (#3754) 2024-01-23 22:21:23 +05:30
Nivedin
27d0a7c437 refactor: persist running requests while switching tabs (#3742) 2024-01-23 22:13:57 +05:30
Nivedin
aca96dd5f2 refactor: add option to disable context menu (#3717) 2024-01-23 22:05:05 +05:30
Anwarul Islam
c0dbcc901f fix: documentation is not being generated on GQL (#3730)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2024-01-23 22:00:41 +05:30
Joel Jacob Stephen
ba52c8cc37 refactor: improvements to the dashboard sidebar (#3709)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 21:55:42 +05:30
Dmitry
d1f6f40ef8 fix: perform logout if the silent refresh attempt fails (#3705)
Co-authored-by: Dmitry Mukovkin <d.mukovkin@cft.ru>
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 21:53:59 +05:30
Akash K
99f5070f71 fix: unwanted double slashes when importing from openapi (#3745) 2024-01-23 21:49:34 +05:30
23 changed files with 222 additions and 234 deletions

View File

@@ -70,7 +70,11 @@
v-model:option-tab="selectedOptionTab" v-model:option-tab="selectedOptionTab"
:properties="properties" :properties="properties"
/> />
<HttpResponse :document="tab.document" :is-embed="true" /> <HttpResponse
v-if="tab.document.response"
:document="tab.document"
:is-embed="true"
/>
</div> </div>
</template> </template>
@@ -88,18 +92,19 @@ import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppTab } from "~/services/tab" import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document" import { HoppRESTDocument } from "~/helpers/rest/document"
import IconSave from "~icons/lucide/save" import IconSave from "~icons/lucide/save"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const props = defineProps<{ const props = defineProps<{
modelTab: HoppTab<HoppRESTDocument> modelTab: HoppTab<HoppRESTDocument>
properties: string[] properties: RESTOptionTabs[]
sharedRequestID: string sharedRequestID: string
}>() }>()
const tab = useModel(props, "modelTab") const tab = useModel(props, "modelTab")
const selectedOptionTab = ref(props.properties[0]) const selectedOptionTab = ref<RESTOptionTabs>(props.properties[0])
const requestCancelFunc: Ref<(() => void) | null> = ref(null) const requestCancelFunc: Ref<(() => void) | null> = ref(null)

View File

@@ -187,7 +187,7 @@ import IconClock from "~icons/lucide/clock"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import IconBox from "~icons/lucide/box" import IconBox from "~icons/lucide/box"
import { computed, nextTick, reactive, ref } from "vue" import { computed, nextTick, reactive, ref } from "vue"
import { GraphQLField, GraphQLType } from "graphql" import { GraphQLField, GraphQLType, getNamedType } from "graphql"
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror" import { useCodemirror } from "@composables/codemirror"
import { copyToClipboard } from "@helpers/utils/clipboard" import { copyToClipboard } from "@helpers/utils/clipboard"
@@ -260,12 +260,6 @@ function getFilteredGraphqlTypes(filterText: string, types: GraphQLType[]) {
}) })
} }
function resolveRootType(type: GraphQLType) {
let t: any = type
while (t.ofType) t = t.ofType
return t
}
const toast = useToast() const toast = useToast()
const downloadSchemaIcon = refAutoReset<typeof IconDownload | typeof IconCheck>( const downloadSchemaIcon = refAutoReset<typeof IconDownload | typeof IconCheck>(
@@ -331,7 +325,7 @@ const handleJumpToType = async (type: GraphQLType) => {
selectedGqlTab.value = "types" selectedGqlTab.value = "types"
await nextTick() await nextTick()
const rootTypeName = resolveRootType(type).name const rootTypeName = getNamedType(type).name
const target = document.getElementById(`type_${rootTypeName}`) const target = document.getElementById(`type_${rootTypeName}`)
if (target) { if (target) {
target.scrollIntoView({ block: "center", behavior: "smooth" }) target.scrollIntoView({ block: "center", behavior: "smooth" })

View File

@@ -8,7 +8,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { GraphQLScalarType, GraphQLType } from "graphql" import { GraphQLScalarType, GraphQLType, getNamedType } from "graphql"
import { computed } from "vue" import { computed } from "vue"
const props = defineProps<{ const props = defineProps<{
@@ -21,15 +21,9 @@ const emit = defineEmits<{
const typeString = computed(() => `${props.gqlType}`) const typeString = computed(() => `${props.gqlType}`)
const isScalar = computed(() => { const isScalar = computed(() => {
return resolveRootType(props.gqlType) instanceof GraphQLScalarType return getNamedType(props.gqlType) instanceof GraphQLScalarType
}) })
function resolveRootType(type: GraphQLType) {
let t = type as any
while (t.ofType !== null) t = t.ofType
return t
}
function jumpToType() { function jumpToType() {
if (isScalar.value) return if (isScalar.value) return
emit("jump-to-type", props.gqlType) emit("jump-to-type", props.gqlType)

View File

@@ -99,6 +99,7 @@ useCodemirror(
linter, linter,
completer, completer,
environmentHighlights: false, environmentHighlights: false,
contextMenuEnabled: false,
}) })
) )

View File

@@ -237,7 +237,7 @@ import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { Ref, computed, onBeforeUnmount, ref } from "vue" import { Ref, computed, ref, onUnmounted } from "vue"
import { defineActionHandler, invokeAction } from "~/helpers/actions" import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { runMutation } from "~/helpers/backend/GQLClient" import { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql" import { UpdateRequestDocument } from "~/helpers/backend/graphql"
@@ -322,6 +322,10 @@ const userHistories = computed(() => {
return history.value.map((history) => history.request.endpoint).slice(0, 10) return history.value.map((history) => history.request.endpoint).slice(0, 10)
}) })
const inspectionService = useService(InspectionService)
const tabs = useService(RESTTabService)
const newSendRequest = async () => { const newSendRequest = async () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) { if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`) toast.error(`${t("empty.endpoint")}`)
@@ -422,6 +426,17 @@ function isCURL(curl: string) {
return curl.includes("curl ") return curl.includes("curl ")
} }
const currentTabID = tabs.currentTabID.value
onUnmounted(() => {
//check if current tab id exist in the current tab id lists
const isCurrentTabRemoved = !tabs
.getActiveTabs()
.value.some((tab) => tab.id === currentTabID)
if (isCurrentTabRemoved) cancelRequest()
})
const cancelRequest = () => { const cancelRequest = () => {
loading.value = false loading.value = false
requestCancelFunc.value?.() requestCancelFunc.value?.()
@@ -553,10 +568,6 @@ const saveRequest = () => {
const request = ref<HoppRESTRequest | null>(null) const request = ref<HoppRESTRequest | null>(null)
onBeforeUnmount(() => {
if (loading.value) cancelRequest()
})
defineActionHandler("request.send-cancel", () => { defineActionHandler("request.send-cancel", () => {
if (!loading.value) newSendRequest() if (!loading.value) newSendRequest()
else cancelRequest() else cancelRequest()
@@ -607,8 +618,5 @@ const isCustomMethod = computed(() => {
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT") const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const inspectionService = useService(InspectionService)
const tabs = useService(RESTTabService)
const tabResults = inspectionService.getResultViewFor(tabs.currentTabID.value) const tabResults = inspectionService.getResultViewFor(tabs.currentTabID.value)
</script> </script>

View File

@@ -5,7 +5,7 @@
render-inactive-tabs render-inactive-tabs
> >
<HoppSmartTab <HoppSmartTab
v-if="properties ? properties.includes('parameters') : true" v-if="properties ? properties.includes('params') : true"
:id="'params'" :id="'params'"
:label="`${t('tab.parameters')}`" :label="`${t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`" :info="`${newActiveParamsCount$}`"
@@ -13,7 +13,7 @@
<HttpParameters v-model="request.params" /> <HttpParameters v-model="request.params" />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
v-if="properties ? properties.includes('body') : true" v-if="properties ? properties.includes('bodyParams') : true"
:id="'bodyParams'" :id="'bodyParams'"
:label="`${t('tab.body')}`" :label="`${t('tab.body')}`"
> >

View File

@@ -95,6 +95,7 @@ useCodemirror(
linter, linter,
completer, completer,
environmentHighlights: false, environmentHighlights: false,
contextMenuEnabled: false,
}) })
) )

View File

@@ -103,7 +103,7 @@ const widgets: Widget[] = [
}, },
] ]
type Tabs = "parameters" | "body" | "headers" | "authorization" type Tabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedOption = { type EmbedOption = {
selectedTab: Tabs selectedTab: Tabs
@@ -116,15 +116,15 @@ type EmbedOption = {
} }
const embedOption = ref<EmbedOption>({ const embedOption = ref<EmbedOption>({
selectedTab: "parameters", selectedTab: "params",
tabs: [ tabs: [
{ {
value: "parameters", value: "params",
label: t("tab.parameters"), label: t("tab.parameters"),
enabled: true, enabled: true,
}, },
{ {
value: "body", value: "bodyParams",
label: t("tab.body"), label: t("tab.body"),
enabled: true, enabled: true,
}, },

View File

@@ -225,10 +225,10 @@ const props = defineProps({
embedOptions: { embedOptions: {
type: Object as PropType<EmbedOption>, type: Object as PropType<EmbedOption>,
default: () => ({ default: () => ({
selectedTab: "parameters", selectedTab: "params",
tabs: [ tabs: [
{ {
value: "parameters", value: "params",
label: "shared_requests.parameters", label: "shared_requests.parameters",
enabled: true, enabled: true,
}, },
@@ -290,7 +290,7 @@ const widgets: Widget[] = [
}, },
] ]
type EmbedTabs = "parameters" | "body" | "headers" | "authorization" type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedOption = { type EmbedOption = {
selectedTab: EmbedTabs selectedTab: EmbedTabs

View File

@@ -56,7 +56,7 @@ import { useI18n } from "~/composables/i18n"
const t = useI18n() const t = useI18n()
type EmbedTabs = "parameters" | "body" | "headers" | "authorization" type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedOption = { type EmbedOption = {
selectedTab: EmbedTabs selectedTab: EmbedTabs
@@ -93,15 +93,15 @@ const props = defineProps({
embedOptions: { embedOptions: {
type: Object as PropType<EmbedOption>, type: Object as PropType<EmbedOption>,
default: () => ({ default: () => ({
selectedTab: "parameters", selectedTab: "params",
tabs: [ tabs: [
{ {
value: "parameters", value: "params",
label: "shared_requests.parameters", label: "shared_requests.parameters",
enabled: true, enabled: true,
}, },
{ {
value: "body", value: "bodyParams",
label: "shared_requests.body", label: "shared_requests.body",
enabled: true, enabled: true,
}, },

View File

@@ -21,7 +21,7 @@
/> />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<div v-if="loading" class="flex flex-col items-center justify-center"> <div v-if="loading" class="flex flex-col items-center justify-center p-4">
<HoppSmartSpinner class="mb-4" /> <HoppSmartSpinner class="mb-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span> <span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div> </div>
@@ -136,15 +136,15 @@ const shareRequestCreatingLoading = ref(false)
const requestToShare = ref<HoppRESTRequest | null>(null) const requestToShare = ref<HoppRESTRequest | null>(null)
const embedOptions = ref<EmbedOption>({ const embedOptions = ref<EmbedOption>({
selectedTab: "parameters", selectedTab: "params",
tabs: [ tabs: [
{ {
value: "parameters", value: "params",
label: t("tab.parameters"), label: t("tab.parameters"),
enabled: false, enabled: false,
}, },
{ {
value: "body", value: "bodyParams",
label: t("tab.body"), label: t("tab.body"),
enabled: false, enabled: false,
}, },
@@ -208,7 +208,7 @@ const currentUser = useReadonlyStream(
const step = ref(1) const step = ref(1)
type EmbedTabs = "parameters" | "body" | "headers" | "authorization" type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedOption = { type EmbedOption = {
selectedTab: EmbedTabs selectedTab: EmbedTabs
@@ -249,7 +249,15 @@ const loading = computed(
onLoggedIn(() => { onLoggedIn(() => {
try { try {
adapter.initialize() // wait for a bit to let the auth token to be set
// because in some race conditions, the token is not set this fixes that
const initLoadTimeout = setTimeout(() => {
adapter.initialize()
}, 10)
return () => {
clearTimeout(initLoadTimeout)
}
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@@ -313,15 +321,15 @@ const displayCustomizeRequestModal = (
info: t("shared_requests.button_info"), info: t("shared_requests.button_info"),
} }
embedOptions.value = { embedOptions.value = {
selectedTab: "parameters", selectedTab: "params",
tabs: [ tabs: [
{ {
value: "parameters", value: "params",
label: t("tab.parameters"), label: t("tab.parameters"),
enabled: false, enabled: false,
}, },
{ {
value: "body", value: "bodyParams",
label: t("tab.body"), label: t("tab.body"),
enabled: false, enabled: false,
}, },
@@ -451,7 +459,7 @@ const getErrorMessage = (err: GQLError<string>) => {
} }
switch (err.error) { switch (err.error) {
case "shortcode/not_found": case "shortcode/not_found":
return t("shared_request.not_found") return t("shared_requests.not_found")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
} }

View File

@@ -57,7 +57,7 @@ import { computed } from "vue"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
type Tabs = "parameters" | "body" | "headers" | "authorization" type Tabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedOption = { type EmbedOption = {
selectedTab: Tabs selectedTab: Tabs

View File

@@ -37,7 +37,7 @@
v-if="currentSuggestionIndex === index" v-if="currentSuggestionIndex === index"
class="hidden items-center text-secondary md:flex" class="hidden items-center text-secondary md:flex"
> >
<kbd class="shortcut-key">TAB</kbd> <kbd class="shortcut-key">Enter</kbd>
<span class="ml-2 truncate">to select</span> <span class="ml-2 truncate">to select</span>
</div> </div>
</li> </li>
@@ -79,6 +79,7 @@ const props = withDefaults(
readonly?: boolean readonly?: boolean
autoCompleteSource?: string[] autoCompleteSource?: string[]
inspectionResults?: InspectorResult[] | undefined inspectionResults?: InspectorResult[] | undefined
contextMenuEnabled?: boolean
}>(), }>(),
{ {
modelValue: "", modelValue: "",
@@ -91,6 +92,7 @@ const props = withDefaults(
autoCompleteSource: undefined, autoCompleteSource: undefined,
inspectionResult: undefined, inspectionResult: undefined,
inspectionResults: undefined, inspectionResults: undefined,
contextMenuEnabled: true,
} }
) )
@@ -167,36 +169,41 @@ watch(
) )
const handleKeystroke = (ev: KeyboardEvent) => { const handleKeystroke = (ev: KeyboardEvent) => {
if (["ArrowDown", "ArrowUp", "Enter", "Tab", "Escape"].includes(ev.key)) { if (!props.autoCompleteSource) return
if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(ev.key)) {
ev.preventDefault() ev.preventDefault()
} }
if (ev.shiftKey) { if (["Escape", "Tab", "Shift"].includes(ev.key)) {
showSuggestionPopover.value = false showSuggestionPopover.value = false
return
} }
showSuggestionPopover.value = true if (ev.key === "Enter") {
if (suggestions.value.length > 0 && currentSuggestionIndex.value > -1) {
updateModelValue(suggestions.value[currentSuggestionIndex.value])
currentSuggestionIndex.value = -1
if ( //used to set codemirror cursor at the end of the line after selecting a suggestion
["Enter", "Tab"].includes(ev.key) && nextTick(() => {
suggestions.value.length > 0 && view.value?.dispatch({
currentSuggestionIndex.value > -1 selection: EditorSelection.create([
) { EditorSelection.range(
updateModelValue(suggestions.value[currentSuggestionIndex.value]) props.modelValue.length,
currentSuggestionIndex.value = -1 props.modelValue.length
),
//used to set codemirror cursor at the end of the line after selecting a suggestion ]),
nextTick(() => { })
view.value?.dispatch({
selection: EditorSelection.create([
EditorSelection.range(
props.modelValue.length,
props.modelValue.length
),
]),
}) })
}) }
if (showSuggestionPopover.value) {
showSuggestionPopover.value = false
} else {
emit("enter", ev)
}
} else {
showSuggestionPopover.value = true
} }
if (ev.key === "ArrowDown") { if (ev.key === "ArrowDown") {
@@ -221,15 +228,6 @@ const handleKeystroke = (ev: KeyboardEvent) => {
emit("keyup", ev) emit("keyup", ev)
} }
if (ev.key === "Enter") {
emit("enter", ev)
showSuggestionPopover.value = false
}
if (ev.key === "Escape") {
showSuggestionPopover.value = false
}
// used to scroll to the first suggestion when left arrow is pressed // used to scroll to the first suggestion when left arrow is pressed
if (ev.key === "ArrowLeft") { if (ev.key === "ArrowLeft") {
if (suggestions.value.length > 0) { if (suggestions.value.length > 0) {
@@ -359,8 +357,11 @@ const initView = (el: any) => {
handleTextSelection() handleTextSelection()
}, 140) }, 140)
el.addEventListener("mouseup", debounceFn) // Only add event listeners if context menu is enabled in the component
el.addEventListener("keyup", debounceFn) if (props.contextMenuEnabled) {
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
}
const extensions: Extension = [ const extensions: Extension = [
EditorView.contentAttributes.of({ "aria-label": props.placeholder }), EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
@@ -396,7 +397,7 @@ const initView = (el: any) => {
ev.preventDefault() ev.preventDefault()
}, },
scroll(event) { scroll(event) {
if (event.target) { if (event.target && props.contextMenuEnabled) {
handleTextSelection() handleTextSelection()
} }
}, },
@@ -405,7 +406,6 @@ const initView = (el: any) => {
class { class {
update(update: ViewUpdate) { update(update: ViewUpdate) {
if (props.readonly) return if (props.readonly) return
if (update.docChanged) { if (update.docChanged) {
const prevValue = clone(cachedValue.value) const prevValue = clone(cachedValue.value)
@@ -436,6 +436,17 @@ const initView = (el: any) => {
clipboardEv = null clipboardEv = null
pastedValue = null pastedValue = null
} }
if (props.contextMenuEnabled) {
// close the context menu if text is being updated in the editor
invokeAction("contextmenu.open", {
position: {
top: 0,
left: 0,
},
text: null,
})
}
} }
} }
} }

View File

@@ -63,6 +63,8 @@ type CodeMirrorOptions = {
additionalExts?: Extension[] additionalExts?: Extension[]
contextMenuEnabled?: boolean
// callback on editor update // callback on editor update
onUpdate?: (view: ViewUpdate) => void onUpdate?: (view: ViewUpdate) => void
} }
@@ -208,6 +210,9 @@ export function useCodemirror(
): { cursor: Ref<{ line: number; ch: number }> } { ): { cursor: Ref<{ line: number; ch: number }> } {
const { subscribeToStream } = useStreamSubscriber() const { subscribeToStream } = useStreamSubscriber()
// Set default value for contextMenuEnabled if not provided
options.contextMenuEnabled = options.contextMenuEnabled ?? true
const additionalExts = new Compartment() const additionalExts = new Compartment()
const language = new Compartment() const language = new Compartment()
const lineWrapping = new Compartment() const lineWrapping = new Compartment()
@@ -272,8 +277,11 @@ export function useCodemirror(
handleTextSelection() handleTextSelection()
}, 140) }, 140)
el.addEventListener("mouseup", debounceFn) // Only add event listeners if context menu is enabled in the editor
el.addEventListener("keyup", debounceFn) if (options.contextMenuEnabled) {
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
}
if (options.onUpdate) { if (options.onUpdate) {
options.onUpdate(update) options.onUpdate(update)
@@ -312,7 +320,7 @@ export function useCodemirror(
), ),
EditorView.domEventHandlers({ EditorView.domEventHandlers({
scroll(event) { scroll(event) {
if (event.target) { if (event.target && options.contextMenuEnabled) {
handleTextSelection() handleTextSelection()
} }
}, },

View File

@@ -154,6 +154,9 @@ export function runRESTRequest$(
) )
if (E.isRight(runResult)) { if (E.isRight(runResult)) {
// set the response in the tab so that multiple tabs can run request simultaneously
tab.value.document.response = res
tab.value.document.testResults = translateToSandboxTestResults( tab.value.document.testResults = translateToSandboxTestResults(
runResult.right runResult.right
) )

View File

@@ -549,13 +549,19 @@ const convertPathToHoppReqs = (
), ),
// Construct request object // Construct request object
RA.map(({ method, info }) => RA.map(({ method, info }) => {
makeRESTRequest({ const openAPIUrl = parseOpenAPIUrl(doc)
const openAPIPath = replaceOpenApiPathTemplating(pathName)
const endpoint =
openAPIUrl.endsWith("/") && openAPIPath.startsWith("/")
? openAPIUrl + openAPIPath.slice(1)
: openAPIUrl + openAPIPath
return makeRESTRequest({
name: info.operationId ?? info.summary ?? "Untitled Request", name: info.operationId ?? info.summary ?? "Untitled Request",
method: method.toUpperCase(), method: method.toUpperCase(),
endpoint: `${parseOpenAPIUrl(doc)}${replaceOpenApiPathTemplating( endpoint,
pathName
)}`,
// We don't need to worry about reference types as the Dereferencing pass should remove them // We don't need to worry about reference types as the Dereferencing pass should remove them
params: parseOpenAPIParams( params: parseOpenAPIParams(
@@ -572,7 +578,7 @@ const convertPathToHoppReqs = (
preRequestScript: "", preRequestScript: "",
testScript: "", testScript: "",
}) })
), }),
// Disable Readonly // Disable Readonly
RA.toArray RA.toArray

View File

@@ -156,6 +156,7 @@ export default class ShortcodeListAdapter {
const [shortcodeCreated$, shortcodeCreatedSub] = runAuthOnlyGQLSubscription( const [shortcodeCreated$, shortcodeCreatedSub] = runAuthOnlyGQLSubscription(
{ {
query: ShortcodeCreatedDocument, query: ShortcodeCreatedDocument,
variables: {},
} }
) )
@@ -172,6 +173,7 @@ export default class ShortcodeListAdapter {
const [shortcodeRevoked$, shortcodeRevokedSub] = runAuthOnlyGQLSubscription( const [shortcodeRevoked$, shortcodeRevokedSub] = runAuthOnlyGQLSubscription(
{ {
query: ShortcodeDeletedDocument, query: ShortcodeDeletedDocument,
variables: {},
} }
) )
@@ -188,6 +190,7 @@ export default class ShortcodeListAdapter {
const [shortcodeUpdated$, shortcodeUpdatedSub] = runAuthOnlyGQLSubscription( const [shortcodeUpdated$, shortcodeUpdatedSub] = runAuthOnlyGQLSubscription(
{ {
query: ShortcodeUpdatedDocument, query: ShortcodeUpdatedDocument,
variables: {},
} }
) )

View File

@@ -1034,6 +1034,11 @@ export default class NewTeamCollectionAdapter {
} }
} }
/**
* Used to obtain the inherited auth and headers for a given folder path, used for both REST and GraphQL team collections
* @param folderPath the path of the folder to cascade the auth from
* @returns the inherited auth and headers for the given folder path
*/
public cascadeParentCollectionForHeaderAuth(folderPath: string) { public cascadeParentCollectionForHeaderAuth(folderPath: string) {
let auth: HoppInheritedProperty["auth"] = { let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "", parentID: folderPath ?? "",
@@ -1080,7 +1085,7 @@ export default class NewTeamCollectionAdapter {
authType: "inherit", authType: "inherit",
authActive: true, authActive: true,
} }
auth.parentID = [...path.slice(0, i + 1)].join("/") auth.parentID = path.slice(0, i + 1).join("/")
auth.parentName = parentFolder.title auth.parentName = parentFolder.title
} }
@@ -1089,9 +1094,12 @@ export default class NewTeamCollectionAdapter {
const parentFolderAuth = data.auth const parentFolderAuth = data.auth
const parentFolderHeaders = data.headers const parentFolderHeaders = data.headers
if (parentFolderAuth?.authType === "inherit" && path.length === 1) { if (
parentFolderAuth?.authType === "inherit" &&
path.slice(0, i + 1).length === 1
) {
auth = { auth = {
parentID: [...path.slice(0, i + 1)].join("/"), parentID: path.slice(0, i + 1).join("/"),
parentName: parentFolder.title, parentName: parentFolder.title,
inheritedAuth: auth.inheritedAuth, inheritedAuth: auth.inheritedAuth,
} }
@@ -1099,7 +1107,7 @@ export default class NewTeamCollectionAdapter {
if (parentFolderAuth?.authType !== "inherit") { if (parentFolderAuth?.authType !== "inherit") {
auth = { auth = {
parentID: [...path.slice(0, i + 1)].join("/"), parentID: path.slice(0, i + 1).join("/"),
parentName: parentFolder.title, parentName: parentFolder.title,
inheritedAuth: parentFolderAuth, inheritedAuth: parentFolderAuth,
} }
@@ -1112,7 +1120,7 @@ export default class NewTeamCollectionAdapter {
const index = headers.findIndex( const index = headers.findIndex(
(h) => h.inheritedHeader?.key === header.key (h) => h.inheritedHeader?.key === header.key
) )
const currentPath = [...path.slice(0, i + 1)].join("/") const currentPath = path.slice(0, i + 1).join("/")
if (index !== -1) { if (index !== -1) {
// Replace the existing header with the same key // Replace the existing header with the same key
headers[index] = { headers[index] = {

View File

@@ -68,7 +68,7 @@ export function navigateToFolderWithIndexPath(
} }
/** /**
* Used to obtain the inherited auth and headers for a given folder path, used for both REST and GraphQL * Used to obtain the inherited auth and headers for a given folder path, used for both REST and GraphQL personal collections
* @param folderPath the path of the folder to cascade the auth from * @param folderPath the path of the folder to cascade the auth from
* @param type the type of collection * @param type the type of collection
* @returns the inherited auth and headers for the given folder path * @returns the inherited auth and headers for the given folder path

View File

@@ -1,7 +1,14 @@
<template> <template>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<div <div
v-if="invalidLink" v-if="sharedRequestDetails.loading"
class="flex justify-center items-center py-5"
>
<HoppSmartSpinner />
</div>
<div
v-else-if="E.isLeft(sharedRequestDetails.data) || invalidLink"
class="flex flex-1 flex-col items-center justify-center p-8" class="flex flex-1 flex-col items-center justify-center p-8"
> >
<icon-lucide-alert-triangle class="svg-icons mb-2 opacity-75" /> <icon-lucide-alert-triangle class="svg-icons mb-2 opacity-75" />
@@ -12,8 +19,9 @@
{{ t("error.invalid_embed_link") }} {{ t("error.invalid_embed_link") }}
</p> </p>
</div> </div>
<Embeds <Embeds
v-else-if="!invalidLink && tab" v-else-if="tab"
v-model:modelTab="tab" v-model:modelTab="tab"
:properties="properties" :properties="properties"
:shared-request-i-d="sharedRequestID" :shared-request-i-d="sharedRequestID"

View File

@@ -114,6 +114,7 @@ async function setInitialUser() {
} else { } else {
setUser(null) setUser(null)
isGettingInitialUser.value = false isGettingInitialUser.value = false
await logout()
} }
return return
@@ -146,22 +147,26 @@ async function setInitialUser() {
} }
async function refreshToken() { async function refreshToken() {
const res = await axios.get( try {
`${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`, const res = await axios.get(
{ `${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`,
withCredentials: true, {
withCredentials: true,
}
)
const isSuccessful = res.status === 200
if (isSuccessful) {
authEvents$.next({
event: "token_refresh",
})
} }
)
const isSuccessful = res.status === 200 return isSuccessful
} catch (error) {
if (isSuccessful) { return false
authEvents$.next({
event: "token_refresh",
})
} }
return isSuccessful
} }
async function sendMagicLink(email: string) { async function sendMagicLink(email: string) {

View File

@@ -1,9 +1,9 @@
// generated by unplugin-vue-components // generated by unplugin-vue-components
// We suggest you to commit this file into source control // We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'; import '@vue/runtime-core'
export {}; export {}
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
@@ -13,8 +13,6 @@ declare module '@vue/runtime-core' {
AppModal: typeof import('./components/app/Modal.vue')['default'] AppModal: typeof import('./components/app/Modal.vue')['default']
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'] AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
AppToast: typeof import('./components/app/Toast.vue')['default'] AppToast: typeof import('./components/app/Toast.vue')['default']
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'] DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'] HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'] HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
@@ -23,6 +21,7 @@ declare module '@vue/runtime-core' {
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'] HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'] HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'] HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'] HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'] HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
@@ -30,42 +29,13 @@ declare module '@vue/runtime-core' {
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable'] HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs'] HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle'] HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'] IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'] IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideUser: typeof import('~icons/lucide/user')['default']
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default'] SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default']
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default'] SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default']
SettingsReset: typeof import('./components/settings/Reset.vue')['default'] SettingsReset: typeof import('./components/settings/Reset.vue')['default']
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default'] SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default']
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default'] SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default']
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
SmartCheckbox: typeof import('./../../hoppscotch-ui/src/components/smart/Checkbox.vue')['default']
SmartConfirmModal: typeof import('./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue')['default']
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
SmartSelectWrapper: typeof import('./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue')['default']
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
SmartTable: typeof import('./../../hoppscotch-ui/src/components/smart/Table.vue')['default']
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
SmartTreeBranch: typeof import('./../../hoppscotch-ui/src/components/smart/TreeBranch.vue')['default']
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
TeamsAdd: typeof import('./components/teams/Add.vue')['default'] TeamsAdd: typeof import('./components/teams/Add.vue')['default']
TeamsDetails: typeof import('./components/teams/Details.vue')['default'] TeamsDetails: typeof import('./components/teams/Details.vue')['default']
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'] TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
@@ -76,27 +46,6 @@ declare module '@vue/runtime-core' {
UsersDetails: typeof import('./components/users/Details.vue')['default'] UsersDetails: typeof import('./components/users/Details.vue')['default']
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'] UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default'] UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default'];
AppLogin: typeof import('./components/app/Login.vue')['default'];
AppLogout: typeof import('./components/app/Logout.vue')['default'];
AppModal: typeof import('./components/app/Modal.vue')['default'];
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'];
AppToast: typeof import('./components/app/Toast.vue')['default'];
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'];
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'];
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'];
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'];
TeamsAdd: typeof import('./components/teams/Add.vue')['default'];
TeamsDetails: typeof import('./components/teams/Details.vue')['default'];
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'];
TeamsMembers: typeof import('./components/teams/Members.vue')['default'];
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'];
Tippy: typeof import('vue-tippy')['Tippy'];
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
} }
} }

View File

@@ -12,7 +12,7 @@
:class="isOpen ? '' : '-translate-x-full ease-in'" :class="isOpen ? '' : '-translate-x-full ease-in'"
class="sidebar-container transform !md:translate-x-0 ease-out" class="sidebar-container transform !md:translate-x-0 ease-out"
> >
<div :class="isExpanded ? 'w-80' : 'w-full'"> <div :class="isExpanded ? 'w-56' : 'w-full'">
<div class="flex items-center justify-start px-4 my-4"> <div class="flex items-center justify-start px-4 my-4">
<div class="flex items-center"> <div class="flex items-center">
<HoppSmartLink class="flex items-center space-x-4" to="/dashboard"> <HoppSmartLink class="flex items-center space-x-4" to="/dashboard">
@@ -26,7 +26,6 @@
</HoppSmartLink> </HoppSmartLink>
</div> </div>
</div> </div>
<nav class="my-5"> <nav class="my-5">
<HoppSmartLink <HoppSmartLink
v-for="(navigation, index) in primaryNavigations" v-for="(navigation, index) in primaryNavigations"
@@ -39,19 +38,32 @@
:to="navigation.to" :to="navigation.to"
tabindex="0" tabindex="0"
:exact="navigation.exact" :exact="navigation.exact"
class="nav-link"
:class=" :class="
!isExpanded !isExpanded
? 'flex items-center justify-center' ? 'flex items-center justify-center'
: 'flex items-center' : 'flex items-center'
" "
@click="setActiveTab(navigation.label)"
> >
<div v-if="navigation.icon"> <div
<component :is="navigation.icon" class="svg-icons" /> class="flex p-5 w-full font-bold"
:class="
activeTab === navigation.label
? 'bg-primaryDark text-secondaryDark border-l-2 border-l-emerald-600'
: 'bg-primary hover:bg-primaryLight hover:text-secondaryDark focus-visible:text-secondaryDark focus-visible:bg-primaryLight focus-visible:outline-none'
"
>
<div
v-if="navigation.icon"
class="svg-icons"
:class="isExpanded ? 'mr-3' : 'mx-auto'"
>
<component :is="navigation.icon" />
</div>
<span v-if="isExpanded" class="nav-title">
{{ navigation.label }}
</span>
</div> </div>
<span v-if="isExpanded" class="nav-title">
{{ navigation.label }}
</span>
</HoppSmartLink> </HoppSmartLink>
</nav> </nav>
</div> </div>
@@ -60,19 +72,27 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { HoppSmartLink } from '@hoppscotch/ui'; import { ref, type Component } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useSidebar } from '~/composables/useSidebar'; import { useSidebar } from '~/composables/useSidebar';
import IconDashboard from '~icons/lucide/layout-dashboard'; import IconDashboard from '~icons/lucide/layout-dashboard';
import IconSettings from '~icons/lucide/settings';
import IconUser from '~icons/lucide/user'; import IconUser from '~icons/lucide/user';
import IconUsers from '~icons/lucide/users'; import IconUsers from '~icons/lucide/users';
import IconSettings from '~icons/lucide/settings';
import { useI18n } from '~/composables/i18n';
const t = useI18n(); const t = useI18n();
const { isOpen, isExpanded } = useSidebar(); const { isOpen, isExpanded } = useSidebar();
const primaryNavigations = [ type NavigationItem = {
label: string;
icon: Component;
to: string;
exact: boolean;
};
const primaryNavigations: NavigationItem[] = [
{ {
label: t('metrics.dashboard'), label: t('metrics.dashboard'),
icon: IconDashboard, icon: IconDashboard,
@@ -98,6 +118,12 @@ const primaryNavigations = [
exact: true, exact: true,
}, },
]; ];
const activeTab = ref('Dashboard');
const setActiveTab = (tab: string) => {
activeTab.value = tab;
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -106,54 +132,4 @@ const primaryNavigations = [
@apply transition duration-300; @apply transition duration-300;
@apply flex overflow-y-auto bg-primary border-r border-divider; @apply flex overflow-y-auto bg-primary border-r border-divider;
} }
.nav-link {
@apply relative;
@apply p-4;
@apply flex flex-1;
@apply items-center;
@apply space-x-4;
@apply hover:bg-primaryDark hover:text-secondaryDark;
@apply focus-visible:text-secondaryDark;
@apply after:absolute;
@apply after:inset-x-0;
@apply after:md:inset-x-auto;
@apply after:md:inset-y-0;
@apply after:bottom-0;
@apply after:md:bottom-auto;
@apply after:md:left-0;
@apply after:z-10;
@apply after:h-0.5;
@apply after:md:h-full;
@apply after:w-full;
@apply after:md:w-0.5;
@apply after:content-[''];
@apply focus:after:bg-divider;
.svg-icons {
@apply opacity-75;
}
&.router-link-active {
@apply text-secondaryDark;
@apply bg-primaryLight;
@apply hover:text-secondaryDark;
@apply after:bg-accent;
.svg-icons {
@apply opacity-100;
}
}
&.exact-active-link {
@apply text-secondaryDark;
@apply bg-primaryLight;
@apply hover:text-secondaryDark;
@apply after:bg-accent;
.svg-icons {
@apply opacity-100;
}
}
}
</style> </style>