Merge branch 'refactor/ui' of https://github.com/hoppscotch/hoppscotch into refactor/ui

This commit is contained in:
liyasthomas
2021-07-30 13:15:10 +05:30
9 changed files with 358 additions and 126 deletions

View File

@@ -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",

View File

@@ -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
View 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
View 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
}

View File

@@ -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"
}

View 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)
}

View File

@@ -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,
}
}

View File

@@ -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,

View File

@@ -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>