Merge branch 'refactor/ui' of https://github.com/hoppscotch/hoppscotch into refactor/ui
This commit is contained in:
@@ -151,7 +151,7 @@ export default defineComponent({
|
||||
this.$emit("handle-import")
|
||||
},
|
||||
copyRequestCode() {
|
||||
this.$clipboard(this.requestCode)
|
||||
;(this.$clipboard as any)(this.requestCode)
|
||||
this.copyIcon = "done"
|
||||
this.$toast.success(this.$t("copied_to_clipboard").toString(), {
|
||||
icon: "done",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
truncate
|
||||
focus:outline-none focus:border-accent
|
||||
"
|
||||
:value="newMethod$"
|
||||
:value="newMethod"
|
||||
autofocus
|
||||
readonly
|
||||
/>
|
||||
@@ -39,10 +39,7 @@
|
||||
:key="`method-${index}`"
|
||||
:label="method"
|
||||
class="font-mono"
|
||||
@click.native="
|
||||
updateMethod(method)
|
||||
$refs.options.tippy().hide()
|
||||
"
|
||||
@click.native="onSelectMethod(method)"
|
||||
/>
|
||||
</tippy>
|
||||
</span>
|
||||
@@ -50,7 +47,7 @@
|
||||
<div class="flex-1 inline-flex">
|
||||
<input
|
||||
id="url"
|
||||
v-model="newEndpoint$"
|
||||
v-model="newEndpoint"
|
||||
class="
|
||||
bg-primaryLight
|
||||
border border-divider
|
||||
@@ -153,7 +150,7 @@
|
||||
<SmartItem
|
||||
ref="copyRequest"
|
||||
:label="$t('copy_request_link')"
|
||||
:icon="navigatorShare ? 'share' : 'content_copy'"
|
||||
:icon="hasNavigatorShare ? 'share' : 'content_copy'"
|
||||
@click.native="
|
||||
copyRequest()
|
||||
$refs.saveOptions.tippy().hide()
|
||||
@@ -187,8 +184,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api"
|
||||
import {
|
||||
updateRESTResponse,
|
||||
restEndpoint$,
|
||||
@@ -200,71 +197,80 @@ import {
|
||||
} from "~/newstore/RESTSession"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
||||
import { useStreamSubscriber, useStream } from "~/helpers/utils/composables"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
|
||||
const methods = [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"CONNECT",
|
||||
"OPTIONS",
|
||||
"TRACE",
|
||||
"PATCH",
|
||||
"CUSTOM",
|
||||
]
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
requestName: useRESTRequestName(),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newMethod$: "",
|
||||
methods: [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"CONNECT",
|
||||
"OPTIONS",
|
||||
"TRACE",
|
||||
"PATCH",
|
||||
"CUSTOM",
|
||||
],
|
||||
name: "",
|
||||
newEndpoint$: "",
|
||||
showCurlImportModal: false,
|
||||
showCodegenModal: false,
|
||||
navigatorShare: navigator.share,
|
||||
loading: false,
|
||||
showSaveRequestModal: false,
|
||||
}
|
||||
},
|
||||
subscriptions() {
|
||||
return {
|
||||
newMethod$: restMethod$,
|
||||
newEndpoint$: restEndpoint$,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
newEndpoint$(newVal) {
|
||||
setRESTEndpoint(newVal)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getSpecialKey: getPlatformSpecialKey,
|
||||
updateMethod(method) {
|
||||
updateRESTMethod(method)
|
||||
},
|
||||
newSendRequest() {
|
||||
this.loading = true
|
||||
this.$subscribeTo(
|
||||
const {
|
||||
$toast,
|
||||
app: { i18n },
|
||||
} = useContext()
|
||||
const t = i18n.t.bind(i18n)
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
const newEndpoint = useStream(restEndpoint$, "", setRESTEndpoint)
|
||||
const newMethod = useStream(restMethod$, "", updateRESTMethod)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const showCurlImportModal = ref(false)
|
||||
const showCodegenModal = ref(false)
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
const hasNavigatorShare = !!navigator.share
|
||||
|
||||
const newSendRequest = () => {
|
||||
loading.value = true
|
||||
|
||||
subscribeToStream(
|
||||
runRESTRequest$(),
|
||||
(responseState) => {
|
||||
console.log(responseState)
|
||||
updateRESTResponse(responseState)
|
||||
if (loading.value) {
|
||||
// Check exists because, loading can be set to false
|
||||
// when cancelled
|
||||
updateRESTResponse(responseState)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.loading = false
|
||||
loading.value = false
|
||||
},
|
||||
() => {
|
||||
this.loading = false
|
||||
loading.value = false
|
||||
}
|
||||
)
|
||||
},
|
||||
copyRequest() {
|
||||
if (navigator.share) {
|
||||
}
|
||||
|
||||
const cancelRequest = () => {
|
||||
loading.value = false
|
||||
updateRESTResponse(null)
|
||||
}
|
||||
|
||||
const updateMethod = (method: string) => {
|
||||
updateRESTMethod(method)
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
resetRESTRequest()
|
||||
}
|
||||
|
||||
const copyRequest = () => {
|
||||
if (!navigator.share) {
|
||||
const time = new Date().toLocaleTimeString()
|
||||
const date = new Date().toLocaleDateString()
|
||||
navigator
|
||||
@@ -276,14 +282,80 @@ export default defineComponent({
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
} else {
|
||||
this.$clipboard(window.location.href)
|
||||
this.$toast.info(this.$t("copied_to_clipboard"), {
|
||||
copyToClipboard(window.location.href)
|
||||
$toast.info(t("copied_to_clipboard").toString(), {
|
||||
icon: "done",
|
||||
})
|
||||
}
|
||||
},
|
||||
clearContent() {
|
||||
resetRESTRequest()
|
||||
}
|
||||
|
||||
const cycleUpMethod = () => {
|
||||
const currentIndex = methods.indexOf(newMethod.value)
|
||||
if (currentIndex === -1) {
|
||||
// Most probs we are in CUSTOM mode
|
||||
// Cycle up from CUSTOM is PATCH
|
||||
updateMethod("PATCH")
|
||||
} else if (currentIndex === 0) {
|
||||
updateMethod("CUSTOM")
|
||||
} else {
|
||||
updateMethod(methods[currentIndex - 1])
|
||||
}
|
||||
}
|
||||
const cycleDownMethod = () => {
|
||||
const currentIndex = methods.indexOf(newMethod.value)
|
||||
if (currentIndex === -1) {
|
||||
// Most probs we are in CUSTOM mode
|
||||
// Cycle down from CUSTOM is GET
|
||||
updateMethod("GET")
|
||||
} else if (currentIndex === methods.length - 1) {
|
||||
updateMethod("GET")
|
||||
} else {
|
||||
updateMethod(methods[currentIndex + 1])
|
||||
}
|
||||
}
|
||||
|
||||
defineActionHandler("request.send-cancel", () => {
|
||||
if (!loading.value) newSendRequest()
|
||||
else cancelRequest()
|
||||
})
|
||||
defineActionHandler("request.reset", clearContent)
|
||||
defineActionHandler("request.copy-link", copyRequest)
|
||||
defineActionHandler("request.method.next", cycleDownMethod)
|
||||
defineActionHandler("request.method.prev", cycleUpMethod)
|
||||
defineActionHandler(
|
||||
"request.save",
|
||||
() => (showSaveRequestModal.value = true)
|
||||
)
|
||||
defineActionHandler("request.method.get", () => updateMethod("GET"))
|
||||
defineActionHandler("request.method.post", () => updateMethod("POST"))
|
||||
defineActionHandler("request.method.put", () => updateMethod("PUT"))
|
||||
defineActionHandler("request.method.delete", () => updateMethod("DELETE"))
|
||||
defineActionHandler("request.method.head", () => updateMethod("HEAD"))
|
||||
|
||||
return {
|
||||
$t: t,
|
||||
$toast,
|
||||
newEndpoint,
|
||||
newMethod,
|
||||
methods,
|
||||
loading,
|
||||
newSendRequest,
|
||||
requestName: useRESTRequestName(),
|
||||
getSpecialKey: getPlatformSpecialKey,
|
||||
showCurlImportModal,
|
||||
showCodegenModal,
|
||||
showSaveRequestModal,
|
||||
hasNavigatorShare,
|
||||
updateMethod,
|
||||
clearContent,
|
||||
copyRequest,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSelectMethod(method: string) {
|
||||
this.updateMethod(method)
|
||||
// Something weird with prettier
|
||||
;(this.$refs.options as any).tippy().hide()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
53
helpers/actions.ts
Normal file
53
helpers/actions.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/* An `action` is a unique verb that is associated with certain thing that can be done on Hoppscotch.
|
||||
* For example, sending a request.
|
||||
*/
|
||||
|
||||
import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api"
|
||||
|
||||
export type HoppAction =
|
||||
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
|
||||
| "request.reset" // Clear request data
|
||||
| "request.copy-link" // Copy Request Link
|
||||
| "request.save" // Save to Collections
|
||||
| "request.method.next" // Select Next Method
|
||||
| "request.method.prev" // Select Previous Method
|
||||
| "request.method.get" // Select GET Method
|
||||
| "request.method.head" // Select HEAD Method
|
||||
| "request.method.post" // Select POST Method
|
||||
| "request.method.put" // Select PUT Method
|
||||
| "request.method.delete" // Select DELETE Method
|
||||
|
||||
type BoundActionList = {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[_ in HoppAction]?: Array<() => void>
|
||||
}
|
||||
|
||||
const boundActions: BoundActionList = {}
|
||||
|
||||
export function bindAction(action: HoppAction, handler: () => void) {
|
||||
if (boundActions[action]) {
|
||||
boundActions[action]?.push(handler)
|
||||
} else {
|
||||
boundActions[action] = [handler]
|
||||
}
|
||||
}
|
||||
|
||||
export function invokeAction(action: HoppAction) {
|
||||
boundActions[action]?.forEach((handler) => handler())
|
||||
}
|
||||
|
||||
export function unbindAction(action: HoppAction, handler: () => void) {
|
||||
boundActions[action] = boundActions[action]?.filter((x) => x !== handler)
|
||||
}
|
||||
|
||||
export function defineActionHandler(action: HoppAction, handler: () => void) {
|
||||
onMounted(() => {
|
||||
bindAction(action, handler)
|
||||
console.log(`Action bound: ${action}`)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unbindAction(action, handler)
|
||||
console.log(`Action unbound: ${action}`)
|
||||
})
|
||||
}
|
||||
107
helpers/keybindings.ts
Normal file
107
helpers/keybindings.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api"
|
||||
import { HoppAction, invokeAction } from "./actions"
|
||||
import { isAppleDevice } from "./platformutils"
|
||||
|
||||
/**
|
||||
* Alt is also regarded as macOS OPTION (⌥) key
|
||||
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
|
||||
*/
|
||||
type ModifierKeys = "ctrl" | "alt"
|
||||
|
||||
/* eslint-disable prettier/prettier */
|
||||
type Key = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k'
|
||||
| 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x'
|
||||
| 'y' | 'z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
|
||||
| "up" | "down" | "left" | "right"
|
||||
/* eslint-enable */
|
||||
|
||||
type ShortcutKey = `${ModifierKeys}-${Key}`
|
||||
|
||||
export const bindings: {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[_ in ShortcutKey]?: HoppAction
|
||||
} = {
|
||||
"ctrl-g": "request.send-cancel",
|
||||
"ctrl-i": "request.reset",
|
||||
"ctrl-k": "request.copy-link",
|
||||
"ctrl-s": "request.save",
|
||||
"alt-up": "request.method.next",
|
||||
"alt-down": "request.method.prev",
|
||||
"alt-g": "request.method.get",
|
||||
"alt-h": "request.method.head",
|
||||
"alt-p": "request.method.post",
|
||||
"alt-u": "request.method.put",
|
||||
"alt-x": "request.method.delete",
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that hooks to the caller component's
|
||||
* lifecycle and hooks to the keyboard events to fire
|
||||
* the appropriate actions based on keybindings
|
||||
*/
|
||||
export function hookKeybindingsListener() {
|
||||
onMounted(() => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
}
|
||||
|
||||
function handleKeyDown(ev: KeyboardEvent) {
|
||||
const binding = generateKeybindingString(ev)
|
||||
if (!binding) return
|
||||
|
||||
const boundAction = bindings[binding]
|
||||
if (!boundAction) return
|
||||
|
||||
ev.preventDefault()
|
||||
invokeAction(boundAction)
|
||||
}
|
||||
|
||||
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||
// All our keybinds need to have one modifier pressed atleast
|
||||
const modifierKey = getActiveModifier(ev)
|
||||
if (!modifierKey) return null
|
||||
|
||||
const key = getPressedKey(ev)
|
||||
if (!key) return null
|
||||
|
||||
return `${modifierKey}-${key}` as ShortcutKey
|
||||
}
|
||||
|
||||
function getPressedKey(ev: KeyboardEvent): Key | null {
|
||||
const val = ev.key.toLowerCase()
|
||||
|
||||
// Check arrow keys
|
||||
if (val === "arrowup") return "up"
|
||||
else if (val === "arrowdown") return "down"
|
||||
else if (val === "arrowleft") return "left"
|
||||
else if (val === "arrowright") return "right"
|
||||
|
||||
// Check letter keys
|
||||
if (val.length === 1 && val.toUpperCase() !== val.toLowerCase())
|
||||
return val as Key
|
||||
|
||||
// Check if number keys
|
||||
if (val.length === 1 && !isNaN(val as any)) return val as Key
|
||||
|
||||
// If no other cases match, this is not a valid key
|
||||
return null
|
||||
}
|
||||
|
||||
function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null {
|
||||
// Just ignore everything if Shift is pressed (for now)
|
||||
if (ev.shiftKey) return null
|
||||
|
||||
// We only allow one modifier key to be pressed (for now)
|
||||
// Control key (+ Command) gets priority and if Alt is also pressed, it is ignored
|
||||
if (isAppleDevice() && ev.metaKey) return "ctrl"
|
||||
else if (!isAppleDevice() && ev.ctrlKey) return "ctrl"
|
||||
|
||||
// Test for Alt key
|
||||
if (ev.altKey) return "alt"
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
export function isAppleDevice() {
|
||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
|
||||
}
|
||||
|
||||
export function getPlatformSpecialKey() {
|
||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? "⌘" : "Ctrl"
|
||||
return isAppleDevice() ? "⌘" : "Ctrl"
|
||||
}
|
||||
|
||||
export function getPlatformAlternateKey() {
|
||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? "⌥" : "Alt"
|
||||
return isAppleDevice() ? "⌥" : "Alt"
|
||||
}
|
||||
|
||||
14
helpers/utils/clipboard.ts
Normal file
14
helpers/utils/clipboard.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copies a given string to the clipboard using
|
||||
* the legacy exec method
|
||||
*
|
||||
* @param content The content to be copied
|
||||
*/
|
||||
export function copyToClipboard(content: string) {
|
||||
const dummy = document.createElement("input")
|
||||
document.body.appendChild(dummy)
|
||||
dummy.value = content
|
||||
dummy.select()
|
||||
document.execCommand("copy")
|
||||
document.body.removeChild(dummy)
|
||||
}
|
||||
@@ -88,3 +88,37 @@ export function pluckRef<T, K extends keyof T>(ref: Ref<T>, key: K): Ref<T[K]> {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that provides the ability to run streams
|
||||
* and subscribe to them and respect the component lifecycle.
|
||||
*/
|
||||
export function useStreamSubscriber() {
|
||||
const subs: Subscription[] = []
|
||||
|
||||
const runAndSubscribe = <T>(
|
||||
stream: Observable<T>,
|
||||
next: (value: T) => void,
|
||||
error: (e: any) => void,
|
||||
complete: () => void
|
||||
) => {
|
||||
const sub = stream.subscribe({
|
||||
next,
|
||||
error,
|
||||
complete: () => {
|
||||
complete()
|
||||
subs.splice(subs.indexOf(sub), 1)
|
||||
},
|
||||
})
|
||||
|
||||
subs.push(sub)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
subs.forEach((sub) => sub.unsubscribe())
|
||||
})
|
||||
|
||||
return {
|
||||
subscribeToStream: runAndSubscribe,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,13 @@ import { registerApolloAuthUpdate } from "~/helpers/apollo"
|
||||
import { initializeFirebase } from "~/helpers/fb"
|
||||
import { getSettingSubject } from "~/newstore/settings"
|
||||
import { logPageView } from "~/helpers/fb/analytics"
|
||||
import { hookKeybindingsListener } from "~/helpers/keybindings"
|
||||
|
||||
export default defineComponent({
|
||||
components: { Splitpanes, Pane },
|
||||
setup() {
|
||||
hookKeybindingsListener()
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
LEFT_SIDEBAR: null,
|
||||
|
||||
@@ -1546,59 +1546,6 @@ export default defineComponent({
|
||||
},
|
||||
async mounted() {
|
||||
restRequest$.subscribe((x) => console.log(x))
|
||||
this._keyListener = function (e) {
|
||||
if (e.key === "g" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
if (!this.runningRequest) {
|
||||
this.sendRequest()
|
||||
} else {
|
||||
this.cancelRequest()
|
||||
}
|
||||
}
|
||||
if (e.key === "s" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
this.saveRequest()
|
||||
}
|
||||
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
this.copyRequest()
|
||||
}
|
||||
if (e.key === "i" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
this.$refs.clearAll.click()
|
||||
}
|
||||
if ((e.key === "g" || e.key === "G") && e.altKey) {
|
||||
this.method = "GET"
|
||||
}
|
||||
if ((e.key === "h" || e.key === "H") && e.altKey) {
|
||||
this.method = "HEAD"
|
||||
}
|
||||
if ((e.key === "p" || e.key === "P") && e.altKey) {
|
||||
this.method = "POST"
|
||||
}
|
||||
if ((e.key === "u" || e.key === "U") && e.altKey) {
|
||||
this.method = "PUT"
|
||||
}
|
||||
if ((e.key === "x" || e.key === "X") && e.altKey) {
|
||||
this.method = "DELETE"
|
||||
}
|
||||
if (e.key == "ArrowUp" && e.altKey && this.currentMethodIndex > 0) {
|
||||
this.method =
|
||||
this.methodMenuItems[
|
||||
--this.currentMethodIndex % this.methodMenuItems.length
|
||||
]
|
||||
} else if (
|
||||
e.key == "ArrowDown" &&
|
||||
e.altKey &&
|
||||
this.currentMethodIndex < 9
|
||||
) {
|
||||
this.method =
|
||||
this.methodMenuItems[
|
||||
++this.currentMethodIndex % this.methodMenuItems.length
|
||||
]
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", this._keyListener.bind(this))
|
||||
await this.oauthRedirectReq()
|
||||
},
|
||||
created() {
|
||||
@@ -1625,8 +1572,5 @@ export default defineComponent({
|
||||
}
|
||||
)
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener("keydown", this._keyListener)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user