feat: auto-complete recent history entries in URL bar (#3141)

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
Nivedin
2023-07-11 20:03:42 +05:30
committed by GitHub
parent 9dbce74f5e
commit c3531c9d8b
4 changed files with 244 additions and 16 deletions

View File

@@ -173,6 +173,7 @@
"folder": "Folder is empty",
"headers": "This request does not have any headers",
"history": "History is empty",
"history_suggestions": "History does not have any matching entries",
"invites": "Invite list is empty",
"members": "Team is empty",
"parameters": "This request does not have any parameters",

View File

@@ -44,11 +44,7 @@
:text="`${t('state.nothing_found')} ‟${filterText}”`"
>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
<span class="my-2 text-center flex flex-col">
{{ t("state.nothing_found") }}
<span class="break-all">"{{ filterText }}"</span>
</span>
</div>
</HoppSmartPlaceholder>
</div>
<div v-else class="flex flex-col divide-y divide-dividerLight">
<details

View File

@@ -1,6 +1,6 @@
<template>
<div
class="sticky top-0 z-20 flex-none flex-shrink-0 p-4 overflow-x-auto sm:flex sm:flex-shrink-0 sm:space-x-2 bg-primary"
class="sticky top-0 z-20 flex-none flex-shrink-0 p-4 sm:flex sm:flex-shrink-0 sm:space-x-2 bg-primary"
>
<div
class="flex flex-1 border rounded min-w-52 border-divider whitespace-nowrap"
@@ -47,13 +47,14 @@
</label>
</div>
<div
class="flex flex-1 overflow-auto transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
class="flex flex-1 transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
>
<SmartEnvInput
v-model="tab.document.request.endpoint"
:placeholder="`${t('request.url')}`"
@enter="newSendRequest()"
:auto-complete-source="userHistories"
@paste="onPasteUrl($event)"
@enter="newSendRequest"
/>
</div>
</div>
@@ -228,7 +229,7 @@
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useSetting } from "@composables/settings"
import { useStreamSubscriber } from "@composables/stream"
import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { refAutoReset, useVModel } from "@vueuse/core"
import * as E from "fp-ts/Either"
@@ -259,6 +260,7 @@ import IconSave from "~icons/lucide/save"
import IconShare2 from "~icons/lucide/share-2"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
import { platform } from "~/platform"
import { getCurrentStrategyID } from "~/helpers/network"
@@ -313,6 +315,12 @@ const clearAll = ref<any | null>(null)
const copyRequestAction = ref<any | null>(null)
const saveRequestAction = ref<any | null>(null)
const history = useReadonlyStream<RESTHistoryEntry[]>(restHistory$, [])
const userHistories = computed(() => {
return history.value.map((history) => history.request.endpoint).slice(0, 10)
})
const newSendRequest = async () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`)

View File

@@ -1,19 +1,44 @@
<template>
<div
class="relative flex items-center flex-1 flex-shrink-0 py-4 overflow-auto whitespace-nowrap"
>
<div class="absolute inset-0 flex flex-1">
<div class="autocomplete-wrapper">
<div class="absolute inset-0 flex flex-1 overflow-x-auto">
<div
ref="editor"
:placeholder="placeholder"
class="flex flex-1"
:class="styles"
@keydown.enter.prevent="emit('enter', $event)"
@keyup="emit('keyup', $event)"
@click="emit('click', $event)"
@keydown="emit('keydown', $event)"
@keydown="handleKeystroke"
@focusin="showSuggestionPopover = true"
></div>
</div>
<ul
v-if="showSuggestionPopover && autoCompleteSource"
ref="suggestionsMenu"
class="suggestions"
>
<li
v-for="(suggestion, index) in suggestions"
:key="`suggestion-${index}`"
:class="{ active: currentSuggestionIndex === index }"
@click="updateModelValue(suggestion)"
>
<span class="truncate py-0.5">
{{ suggestion }}
</span>
<div
v-if="currentSuggestionIndex === index"
class="hidden md:flex text-secondary items-center"
>
<kbd class="shortcut-key">TAB</kbd>
<span class="ml-2 truncate">to select</span>
</div>
</li>
<li v-if="suggestions.length === 0" class="pointer-events-none">
<span class="truncate py-0.5">
{{ t("empty.history_suggestions") }}
</span>
</li>
</ul>
</div>
</template>
@@ -35,6 +60,8 @@ import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironme
import { useReadonlyStream } from "@composables/stream"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { platform } from "~/platform"
import { useI18n } from "~/composables/i18n"
import { onClickOutside } from "@vueuse/core"
const props = withDefaults(
defineProps<{
@@ -46,6 +73,7 @@ const props = withDefaults(
selectTextOnMount?: boolean
environmentHighlights?: boolean
readonly?: boolean
autoCompleteSource?: string[]
}>(),
{
modelValue: "",
@@ -55,6 +83,7 @@ const props = withDefaults(
focus: false,
readonly: false,
environmentHighlights: true,
autoCompleteSource: undefined,
}
)
@@ -68,12 +97,160 @@ const emit = defineEmits<{
(e: "click", ev: any): void
}>()
const t = useI18n()
const cachedValue = ref(props.modelValue)
const view = ref<EditorView>()
const editor = ref<any | null>(null)
const currentSuggestionIndex = ref(-1)
const showSuggestionPopover = ref(false)
const suggestionsMenu = ref<any | null>(null)
onClickOutside(suggestionsMenu, () => {
showSuggestionPopover.value = false
})
//filter autocompleteSource with unique values
const uniqueAutoCompleteSource = computed(() => {
if (props.autoCompleteSource) {
return [...new Set(props.autoCompleteSource)]
} else {
return []
}
})
const suggestions = computed(() => {
if (
props.modelValue &&
props.modelValue.length > 0 &&
uniqueAutoCompleteSource.value &&
uniqueAutoCompleteSource.value.length > 0
) {
return uniqueAutoCompleteSource.value.filter((suggestion) =>
suggestion.toLowerCase().includes(props.modelValue.toLowerCase())
)
} else {
return uniqueAutoCompleteSource.value ?? []
}
})
const updateModelValue = (value: string) => {
emit("update:modelValue", value)
emit("change", value)
showSuggestionPopover.value = false
}
const handleKeystroke = (ev: KeyboardEvent) => {
if (["ArrowDown", "ArrowUp", "Enter", "Tab", "Escape"].includes(ev.key)) {
ev.preventDefault()
}
showSuggestionPopover.value = true
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
),
]),
})
})
}
if (ev.key === "ArrowDown") {
scrollActiveElIntoView()
currentSuggestionIndex.value =
currentSuggestionIndex.value < suggestions.value.length - 1
? currentSuggestionIndex.value + 1
: suggestions.value.length - 1
emit("keydown", ev)
}
if (ev.key === "ArrowUp") {
scrollActiveElIntoView()
currentSuggestionIndex.value =
currentSuggestionIndex.value - 1 >= 0
? currentSuggestionIndex.value - 1
: 0
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) {
currentSuggestionIndex.value = 0
nextTick(() => {
scrollActiveElIntoView()
})
}
}
// used to scroll to the last suggestion when right arrow is pressed
if (ev.key === "ArrowRight") {
if (suggestions.value.length > 0) {
currentSuggestionIndex.value = suggestions.value.length - 1
nextTick(() => {
scrollActiveElIntoView()
})
}
}
}
// reset currentSuggestionIndex showSuggestionPopover is false
watch(
() => showSuggestionPopover.value,
(newVal) => {
if (!newVal) {
currentSuggestionIndex.value = -1
}
}
)
/**
* Used to scroll the active suggestion into view
*/
const scrollActiveElIntoView = () => {
const suggestionsMenuEl = suggestionsMenu.value
if (suggestionsMenuEl) {
const activeSuggestionEl = suggestionsMenuEl.querySelector(".active")
if (activeSuggestionEl) {
activeSuggestionEl.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "start",
})
}
}
}
watch(
() => props.modelValue,
(newVal) => {
@@ -236,3 +413,49 @@ watch(editor, () => {
}
})
</script>
<style lang="scss" scoped>
.autocomplete-wrapper {
@apply relative;
@apply flex;
@apply flex-1;
@apply flex-shrink-0;
@apply whitespace-nowrap;
.suggestions {
@apply absolute;
@apply bg-popover;
@apply z-50;
@apply shadow-lg;
@apply max-h-46;
@apply border-b border-x border-divider;
@apply overflow-y-auto;
@apply -left-[1px];
@apply right-0;
top: calc(100% + 1px);
border-radius: 0 0 8px 8px;
li {
@apply flex;
@apply items-center;
@apply justify-between;
@apply w-full;
@apply py-2 px-4;
@apply text-secondary;
@apply cursor-pointer;
&:last-child {
border-radius: 0 0 0 8px;
}
&:hover,
&.active {
@apply bg-primaryDark;
@apply text-secondaryDark;
@apply cursor-pointer;
}
}
}
}
</style>