Compare commits

...

15 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
17 changed files with 146 additions and 82 deletions

View File

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

View File

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

View File

@@ -237,7 +237,7 @@ import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { useVModel } from "@vueuse/core"
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 { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
@@ -322,6 +322,10 @@ const userHistories = computed(() => {
return history.value.map((history) => history.request.endpoint).slice(0, 10)
})
const inspectionService = useService(InspectionService)
const tabs = useService(RESTTabService)
const newSendRequest = async () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`)
@@ -422,6 +426,17 @@ function isCURL(curl: string) {
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 = () => {
loading.value = false
requestCancelFunc.value?.()
@@ -553,10 +568,6 @@ const saveRequest = () => {
const request = ref<HoppRESTRequest | null>(null)
onBeforeUnmount(() => {
if (loading.value) cancelRequest()
})
defineActionHandler("request.send-cancel", () => {
if (!loading.value) newSendRequest()
else cancelRequest()
@@ -607,8 +618,5 @@ const isCustomMethod = computed(() => {
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const inspectionService = useService(InspectionService)
const tabs = useService(RESTTabService)
const tabResults = inspectionService.getResultViewFor(tabs.currentTabID.value)
</script>

View File

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

View File

@@ -95,6 +95,7 @@ useCodemirror(
linter,
completer,
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 = {
selectedTab: Tabs
@@ -116,15 +116,15 @@ type EmbedOption = {
}
const embedOption = ref<EmbedOption>({
selectedTab: "parameters",
selectedTab: "params",
tabs: [
{
value: "parameters",
value: "params",
label: t("tab.parameters"),
enabled: true,
},
{
value: "body",
value: "bodyParams",
label: t("tab.body"),
enabled: true,
},

View File

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

View File

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

View File

@@ -21,7 +21,7 @@
/>
</div>
<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" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
@@ -136,15 +136,15 @@ const shareRequestCreatingLoading = ref(false)
const requestToShare = ref<HoppRESTRequest | null>(null)
const embedOptions = ref<EmbedOption>({
selectedTab: "parameters",
selectedTab: "params",
tabs: [
{
value: "parameters",
value: "params",
label: t("tab.parameters"),
enabled: false,
},
{
value: "body",
value: "bodyParams",
label: t("tab.body"),
enabled: false,
},
@@ -208,7 +208,7 @@ const currentUser = useReadonlyStream(
const step = ref(1)
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedOption = {
selectedTab: EmbedTabs
@@ -249,7 +249,15 @@ const loading = computed(
onLoggedIn(() => {
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) {
console.error(e)
}
@@ -313,15 +321,15 @@ const displayCustomizeRequestModal = (
info: t("shared_requests.button_info"),
}
embedOptions.value = {
selectedTab: "parameters",
selectedTab: "params",
tabs: [
{
value: "parameters",
value: "params",
label: t("tab.parameters"),
enabled: false,
},
{
value: "body",
value: "bodyParams",
label: t("tab.body"),
enabled: false,
},
@@ -451,7 +459,7 @@ const getErrorMessage = (err: GQLError<string>) => {
}
switch (err.error) {
case "shortcode/not_found":
return t("shared_request.not_found")
return t("shared_requests.not_found")
default:
return t("error.something_went_wrong")
}

View File

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

View File

@@ -37,7 +37,7 @@
v-if="currentSuggestionIndex === index"
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>
</div>
</li>
@@ -79,6 +79,7 @@ const props = withDefaults(
readonly?: boolean
autoCompleteSource?: string[]
inspectionResults?: InspectorResult[] | undefined
contextMenuEnabled?: boolean
}>(),
{
modelValue: "",
@@ -91,6 +92,7 @@ const props = withDefaults(
autoCompleteSource: undefined,
inspectionResult: undefined,
inspectionResults: undefined,
contextMenuEnabled: true,
}
)
@@ -167,36 +169,41 @@ watch(
)
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()
}
if (ev.shiftKey) {
if (["Escape", "Tab", "Shift"].includes(ev.key)) {
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 (
["Enter", "Tab"].includes(ev.key) &&
suggestions.value.length > 0 &&
currentSuggestionIndex.value > -1
) {
updateModelValue(suggestions.value[currentSuggestionIndex.value])
currentSuggestionIndex.value = -1
//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
),
]),
//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") {
@@ -221,15 +228,6 @@ const handleKeystroke = (ev: KeyboardEvent) => {
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
if (ev.key === "ArrowLeft") {
if (suggestions.value.length > 0) {
@@ -359,8 +357,11 @@ const initView = (el: any) => {
handleTextSelection()
}, 140)
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
// Only add event listeners if context menu is enabled in the component
if (props.contextMenuEnabled) {
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
}
const extensions: Extension = [
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
@@ -396,7 +397,7 @@ const initView = (el: any) => {
ev.preventDefault()
},
scroll(event) {
if (event.target) {
if (event.target && props.contextMenuEnabled) {
handleTextSelection()
}
},
@@ -405,7 +406,6 @@ const initView = (el: any) => {
class {
update(update: ViewUpdate) {
if (props.readonly) return
if (update.docChanged) {
const prevValue = clone(cachedValue.value)
@@ -436,6 +436,17 @@ const initView = (el: any) => {
clipboardEv = 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[]
contextMenuEnabled?: boolean
// callback on editor update
onUpdate?: (view: ViewUpdate) => void
}
@@ -208,6 +210,9 @@ export function useCodemirror(
): { cursor: Ref<{ line: number; ch: number }> } {
const { subscribeToStream } = useStreamSubscriber()
// Set default value for contextMenuEnabled if not provided
options.contextMenuEnabled = options.contextMenuEnabled ?? true
const additionalExts = new Compartment()
const language = new Compartment()
const lineWrapping = new Compartment()
@@ -272,8 +277,11 @@ export function useCodemirror(
handleTextSelection()
}, 140)
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
// Only add event listeners if context menu is enabled in the editor
if (options.contextMenuEnabled) {
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
}
if (options.onUpdate) {
options.onUpdate(update)
@@ -312,7 +320,7 @@ export function useCodemirror(
),
EditorView.domEventHandlers({
scroll(event) {
if (event.target) {
if (event.target && options.contextMenuEnabled) {
handleTextSelection()
}
},

View File

@@ -154,6 +154,9 @@ export function runRESTRequest$(
)
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(
runResult.right
)

View File

@@ -156,6 +156,7 @@ export default class ShortcodeListAdapter {
const [shortcodeCreated$, shortcodeCreatedSub] = runAuthOnlyGQLSubscription(
{
query: ShortcodeCreatedDocument,
variables: {},
}
)
@@ -172,6 +173,7 @@ export default class ShortcodeListAdapter {
const [shortcodeRevoked$, shortcodeRevokedSub] = runAuthOnlyGQLSubscription(
{
query: ShortcodeDeletedDocument,
variables: {},
}
)
@@ -188,6 +190,7 @@ export default class ShortcodeListAdapter {
const [shortcodeUpdated$, shortcodeUpdatedSub] = runAuthOnlyGQLSubscription(
{
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) {
let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "",
@@ -1080,7 +1085,7 @@ export default class NewTeamCollectionAdapter {
authType: "inherit",
authActive: true,
}
auth.parentID = [...path.slice(0, i + 1)].join("/")
auth.parentID = path.slice(0, i + 1).join("/")
auth.parentName = parentFolder.title
}
@@ -1089,9 +1094,12 @@ export default class NewTeamCollectionAdapter {
const parentFolderAuth = data.auth
const parentFolderHeaders = data.headers
if (parentFolderAuth?.authType === "inherit" && path.length === 1) {
if (
parentFolderAuth?.authType === "inherit" &&
path.slice(0, i + 1).length === 1
) {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentID: path.slice(0, i + 1).join("/"),
parentName: parentFolder.title,
inheritedAuth: auth.inheritedAuth,
}
@@ -1099,7 +1107,7 @@ export default class NewTeamCollectionAdapter {
if (parentFolderAuth?.authType !== "inherit") {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentID: path.slice(0, i + 1).join("/"),
parentName: parentFolder.title,
inheritedAuth: parentFolderAuth,
}
@@ -1112,7 +1120,7 @@ export default class NewTeamCollectionAdapter {
const index = headers.findIndex(
(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) {
// Replace the existing header with the same key
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 type the type of collection
* @returns the inherited auth and headers for the given folder path

View File

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