feat: desktop app
Co-authored-by: Vivek R <123vivekr@gmail.com> Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
19
packages/hoppscotch-common/src/components.d.ts
vendored
19
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||
@@ -58,6 +58,8 @@ declare module 'vue' {
|
||||
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
||||
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
|
||||
Environments: typeof import('./components/environments/index.vue')['default']
|
||||
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||
@@ -90,13 +92,11 @@ declare module 'vue' {
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
||||
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
||||
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||
@@ -143,7 +143,6 @@ declare module 'vue' {
|
||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
|
||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
@@ -153,10 +152,8 @@ declare module 'vue' {
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||
@@ -189,7 +186,6 @@ declare module 'vue' {
|
||||
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
|
||||
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
||||
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
||||
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.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']
|
||||
@@ -222,4 +218,5 @@ declare module 'vue' {
|
||||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<AppInterceptor />
|
||||
</template>
|
||||
</tippy>
|
||||
<HoppButtonSecondary
|
||||
v-if="platform.platformFeatureFlags.cookiesEnabled ?? false"
|
||||
:label="t('app.cookies')"
|
||||
:icon="IconCookie"
|
||||
@click="showCookiesModal = true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<tippy
|
||||
@@ -195,12 +201,17 @@
|
||||
:show="showDeveloperOptions"
|
||||
@hide-modal="showDeveloperOptions = false"
|
||||
/>
|
||||
<CookiesAllModal
|
||||
:show="showCookiesModal"
|
||||
@hide-modal="showCookiesModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { version } from "~/../package.json"
|
||||
import IconCookie from "~icons/lucide/cookie"
|
||||
import IconSidebar from "~icons/lucide/sidebar"
|
||||
import IconZap from "~icons/lucide/zap"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
@@ -223,7 +234,9 @@ import { invokeAction } from "@helpers/actions"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const showDeveloperOptions = ref(false)
|
||||
const showCookiesModal = ref(false)
|
||||
|
||||
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
||||
const SIDEBAR = useSetting("SIDEBAR")
|
||||
|
||||
@@ -258,7 +258,7 @@ const importFromJSON = () => {
|
||||
inputChooseFileToImportFrom.value.value = ""
|
||||
}
|
||||
|
||||
const exportJSON = () => {
|
||||
const exportJSON = async () => {
|
||||
const dataToWrite = collectionJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
@@ -268,24 +268,32 @@ const exportJSON = () => {
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
platform?.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "gql",
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "Hoppscotch Collection JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// TODO: get uri from meta
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
toast.success(t("state.download_started").toString())
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
platform?.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "gql",
|
||||
})
|
||||
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1866,28 +1866,25 @@ const getJSONCollection = async () => {
|
||||
* @param collectionJSON - JSON string of the collection
|
||||
* @param name - Name of the collection set as the file name
|
||||
*/
|
||||
const initializeDownloadCollection = (
|
||||
const initializeDownloadCollection = async (
|
||||
collectionJSON: string,
|
||||
name: string | null
|
||||
) => {
|
||||
const file = new Blob([collectionJSON], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: collectionJSON,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: `${name ?? "collection"}.json`,
|
||||
filters: [
|
||||
{
|
||||
name: "Hoppscotch Collection JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (name) {
|
||||
a.download = `${name}.json`
|
||||
} else {
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
toast.success(t("state.download_started").toString())
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1916,11 +1913,14 @@ const exportData = async (
|
||||
exportLoading.value = false
|
||||
return
|
||||
},
|
||||
(coll) => {
|
||||
async (coll) => {
|
||||
const hoppColl = teamCollToHoppRESTColl(coll)
|
||||
const collectionJSONString = JSON.stringify(hoppColl)
|
||||
|
||||
initializeDownloadCollection(collectionJSONString, hoppColl.name)
|
||||
await initializeDownloadCollection(
|
||||
collectionJSONString,
|
||||
hoppColl.name
|
||||
)
|
||||
exportLoading.value = false
|
||||
}
|
||||
)
|
||||
|
||||
269
packages/hoppscotch-common/src/components/cookies/AllModal.vue
Normal file
269
packages/hoppscotch-common/src/components/cookies/AllModal.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('app.cookies')"
|
||||
aria-modal="true"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!currentInterceptorSupportsCookies"
|
||||
:text="t('cookies.modal.interceptor_no_support')"
|
||||
>
|
||||
<AppInterceptor class="p-2 border rounded border-dividerLight" />
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="flex flex-col">
|
||||
<div
|
||||
class="flex bg-primary space-x-2 border-b sticky border-dividerLight -mx-4 px-4 py-4 -mt-4"
|
||||
style="top: calc(-1 * var(--line-height-body))"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="newDomainText"
|
||||
class="flex-1"
|
||||
:placeholder="t('cookies.modal.new_domain_name')"
|
||||
@keyup.enter="addNewDomain"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
filled
|
||||
:label="t('action.add')"
|
||||
@click="addNewDomain"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingCookieJar.size === 0"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('cookies.modal.empty_domains')}`"
|
||||
:text="t('cookies.modal.empty_domains')"
|
||||
class="mt-6"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
<div
|
||||
v-for="[domain, entries] in workingCookieJar.entries()"
|
||||
v-else
|
||||
:key="domain"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<div class="flex items-center justify-between flex-1">
|
||||
<label for="cookiesList" class="p-4">
|
||||
{{ domain }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.delete')"
|
||||
:icon="IconTrash2"
|
||||
@click="deleteDomain(domain)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
:icon="IconPlus"
|
||||
@click="addCookieToDomain(domain)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border rounded border-divider">
|
||||
<div class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-if="entries.length === 0"
|
||||
class="flex flex-col gap-2 p-4 items-center"
|
||||
>
|
||||
{{ t("cookies.modal.no_cookies_in_domain") }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(entry, entryIndex) in entries"
|
||||
:key="`${entry}-${entryIndex}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
||||
:value="entry"
|
||||
readonly
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.edit')"
|
||||
:icon="IconEdit"
|
||||
@click="editCookie(domain, entryIndex, entry)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@click="deleteCookie(domain, entryIndex)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="currentInterceptorSupportsCookies" #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
v-focus
|
||||
:label="t('action.save')"
|
||||
outline
|
||||
@click="saveCookieChanges"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="cancelCookieChanges"
|
||||
/>
|
||||
</span>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.clear_all')"
|
||||
outline
|
||||
filled
|
||||
@click="clearAllDomains"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
<CookiesEditCookie
|
||||
:show="!!showEditModalFor"
|
||||
:entry="showEditModalFor"
|
||||
@save-cookie="saveCookie"
|
||||
@hide-modal="showEditModalFor = null"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useService } from "dioc/vue"
|
||||
import { CookieJarService } from "~/services/cookie-jar.service"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconEdit from "~icons/lucide/edit"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { ref, watch, computed } from "vue"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { EditCookieConfig } from "./EditCookie.vue"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
const toast = useToast()
|
||||
|
||||
const newDomainText = ref("")
|
||||
|
||||
const interceptorService = useService(InterceptorService)
|
||||
const cookieJarService = useService(CookieJarService)
|
||||
|
||||
const workingCookieJar = ref(cloneDeep(cookieJarService.cookieJar.value))
|
||||
|
||||
const currentInterceptorSupportsCookies = computed(() => {
|
||||
const currentInterceptor = interceptorService.currentInterceptor.value
|
||||
|
||||
if (!currentInterceptor) return true
|
||||
|
||||
return currentInterceptor.supportsCookies ?? false
|
||||
})
|
||||
|
||||
function addNewDomain() {
|
||||
if (newDomainText.value === "" || /^\s+$/.test(newDomainText.value)) {
|
||||
toast.error(`${t("cookies.modal.empty_domain")}`)
|
||||
return
|
||||
}
|
||||
|
||||
workingCookieJar.value.set(newDomainText.value, [])
|
||||
newDomainText.value = ""
|
||||
}
|
||||
|
||||
function deleteDomain(domain: string) {
|
||||
workingCookieJar.value.delete(domain)
|
||||
}
|
||||
|
||||
function addCookieToDomain(domain: string) {
|
||||
showEditModalFor.value = { type: "create", domain }
|
||||
}
|
||||
|
||||
function clearAllDomains() {
|
||||
workingCookieJar.value = new Map()
|
||||
toast.success(`${t("state.cleared")}`)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
workingCookieJar.value = cloneDeep(cookieJarService.cookieJar.value)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const showEditModalFor = ref<EditCookieConfig | null>(null)
|
||||
|
||||
function saveCookieChanges() {
|
||||
cookieJarService.cookieJar.value = workingCookieJar.value
|
||||
hideModal()
|
||||
}
|
||||
|
||||
function cancelCookieChanges() {
|
||||
hideModal()
|
||||
}
|
||||
|
||||
function editCookie(domain: string, entryIndex: number, cookieEntry: string) {
|
||||
showEditModalFor.value = {
|
||||
type: "edit",
|
||||
domain,
|
||||
entryIndex,
|
||||
currentCookieEntry: cookieEntry,
|
||||
}
|
||||
}
|
||||
|
||||
function deleteCookie(domain: string, entryIndex: number) {
|
||||
const entry = workingCookieJar.value.get(domain)
|
||||
|
||||
if (entry) {
|
||||
entry.splice(entryIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function saveCookie(cookie: string) {
|
||||
if (showEditModalFor.value?.type === "create") {
|
||||
const { domain } = showEditModalFor.value
|
||||
|
||||
const entry = workingCookieJar.value.get(domain)!
|
||||
entry.push(cookie)
|
||||
|
||||
showEditModalFor.value = null
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (showEditModalFor.value?.type !== "edit") return
|
||||
|
||||
const { domain, entryIndex } = showEditModalFor.value!
|
||||
|
||||
const entry = workingCookieJar.value.get(domain)
|
||||
|
||||
if (entry) {
|
||||
entry[entryIndex] = cookie
|
||||
}
|
||||
|
||||
showEditModalFor.value = null
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
195
packages/hoppscotch-common/src/components/cookies/EditCookie.vue
Normal file
195
packages/hoppscotch-common/src/components/cookies/EditCookie.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('cookies.modal.set')"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div class="border rounded border-dividerLight">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center justify-between pl-4">
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
{{ t("cookies.modal.cookie_string") }}
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
:icon="IconTrash2"
|
||||
@click="clearContent()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
:icon="IconWrapText"
|
||||
@click.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.download_file')"
|
||||
:icon="downloadIcon"
|
||||
@click="downloadResponse"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="t('action.copy')"
|
||||
:icon="copyIcon"
|
||||
@click="copyResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-46">
|
||||
<div
|
||||
ref="cookieEditor"
|
||||
class="h-full border-t rounded-b border-dividerLight"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
v-focus
|
||||
:label="t('action.save')"
|
||||
outline
|
||||
@click="saveCookieChange"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="cancelCookieChange"
|
||||
/>
|
||||
</div>
|
||||
<span class="flex">
|
||||
<HoppButtonSecondary
|
||||
:icon="pasteIcon"
|
||||
:label="`${t('action.paste')}`"
|
||||
filled
|
||||
outline
|
||||
@click="handlePaste"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type EditCookieConfig =
|
||||
| { type: "create"; domain: string }
|
||||
| {
|
||||
type: "edit"
|
||||
domain: string
|
||||
entryIndex: number
|
||||
currentCookieEntry: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useCodemirror } from "~/composables/codemirror"
|
||||
import { watch, ref, reactive } from "vue"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconClipboard from "~icons/lucide/clipboard"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import {
|
||||
useCopyResponse,
|
||||
useDownloadResponse,
|
||||
} from "~/composables/lens-actions"
|
||||
|
||||
// TODO: Build Managed Mode!
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
|
||||
entry: EditCookieConfig | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "save-cookie", cookie: string): void
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const cookieEditor = ref<HTMLElement>()
|
||||
const rawCookieString = ref("")
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
cookieEditor,
|
||||
rawCookieString,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
mode: "text/plain",
|
||||
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
|
||||
lineWrapping: linewrapEnabled,
|
||||
},
|
||||
linter: null,
|
||||
completer: null,
|
||||
environmentHighlights: false,
|
||||
})
|
||||
)
|
||||
|
||||
const pasteIcon = refAutoReset<typeof IconClipboard | typeof IconCheck>(
|
||||
IconClipboard,
|
||||
1000
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.entry,
|
||||
() => {
|
||||
if (!props.entry) return
|
||||
|
||||
if (props.entry.type === "create") {
|
||||
rawCookieString.value = ""
|
||||
return
|
||||
}
|
||||
|
||||
rawCookieString.value = props.entry.currentCookieEntry
|
||||
}
|
||||
)
|
||||
|
||||
function hideModal() {
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
function cancelCookieChange() {
|
||||
hideModal()
|
||||
}
|
||||
|
||||
async function handlePaste() {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) {
|
||||
rawCookieString.value = text
|
||||
pasteIcon.value = IconCheck
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to copy: ", e)
|
||||
toast.error(t("profile.no_permission").toString())
|
||||
}
|
||||
}
|
||||
|
||||
function saveCookieChange() {
|
||||
emit("save-cookie", rawCookieString.value)
|
||||
}
|
||||
|
||||
const { copyIcon, copyResponse } = useCopyResponse(rawCookieString)
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
"",
|
||||
rawCookieString
|
||||
)
|
||||
|
||||
function clearContent() {
|
||||
rawCookieString.value = ""
|
||||
}
|
||||
</script>
|
||||
@@ -375,7 +375,7 @@ const importFromPostman = ({
|
||||
importFromHoppscotch(environments)
|
||||
}
|
||||
|
||||
const exportJSON = () => {
|
||||
const exportJSON = async () => {
|
||||
const dataToWrite = environmentJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
@@ -385,19 +385,27 @@ const exportJSON = () => {
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
// TODO: get uri from meta
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
toast.success(t("state.download_started").toString())
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
|
||||
@@ -59,6 +59,7 @@ import { useToast } from "@composables/toast"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -111,21 +112,31 @@ const copyResponse = (str: string) => {
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const downloadResponse = (str: string) => {
|
||||
const downloadResponse = async (str: string) => {
|
||||
const dataToWrite = str
|
||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadResponseIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
downloadResponseIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
}
|
||||
}
|
||||
|
||||
defineActionHandler(
|
||||
|
||||
@@ -202,6 +202,7 @@ import {
|
||||
schemaString,
|
||||
subscriptionFields,
|
||||
} from "~/helpers/graphql/connection"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
type NavigationTabs = "history" | "collection" | "docs" | "schema"
|
||||
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
|
||||
@@ -372,21 +373,33 @@ useCodemirror(
|
||||
})
|
||||
)
|
||||
|
||||
const downloadSchema = () => {
|
||||
const dataToWrite = JSON.stringify(schemaString.value, null, 2)
|
||||
const downloadSchema = async () => {
|
||||
const dataToWrite = schemaString.value
|
||||
const file = new Blob([dataToWrite], { type: "application/graphql" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.graphql`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadSchemaIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
const filename = `${
|
||||
url.split("/").pop()!.split("#")[0].split("?")[0]
|
||||
}.graphql`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/graphql",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "GraphQL Schema File",
|
||||
extensions: ["graphql"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
downloadSchemaIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
}
|
||||
}
|
||||
|
||||
const copySchema = () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useI18n } from "./i18n"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { copyToClipboard } from "@helpers/utils/clipboard"
|
||||
import { HoppRESTResponse } from "@helpers/types/HoppRESTResponse"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
export function useCopyResponse(responseBodyText: Ref<any>) {
|
||||
const toast = useToast()
|
||||
@@ -40,15 +41,14 @@ export function useDownloadResponse(
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
const downloadResponse = () => {
|
||||
const downloadResponse = async () => {
|
||||
const dataToWrite = responseBody.value
|
||||
const file = new Blob([dataToWrite], { type: contentType })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
// TODO: get uri from meta
|
||||
a.download = pipe(
|
||||
// Guess extension and filename
|
||||
const file = new Blob([dataToWrite], { type: contentType })
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
const filename = pipe(
|
||||
url,
|
||||
S.split("/"),
|
||||
RNEA.last,
|
||||
@@ -58,15 +58,24 @@ export function useDownloadResponse(
|
||||
RNEA.head
|
||||
)
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
downloadIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
console.log(filename)
|
||||
|
||||
// TODO: Look at the mime type and determine extension ?
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: contentType,
|
||||
suggestedFilename: filename,
|
||||
})
|
||||
|
||||
// Assume success if unknown as we cannot determine
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
downloadIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
downloadIcon,
|
||||
downloadResponse,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const getEnvironmentJson = (
|
||||
environmentObj: TeamEnvironment | Environment,
|
||||
@@ -32,17 +33,24 @@ export const exportAsJSON = (
|
||||
if (!dataToWrite) return false
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
// Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
// Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
|
||||
suggestedFilename: `${
|
||||
url.split("/").pop()!.split("#")[0].split("?")[0]
|
||||
}.json`,
|
||||
filters: [
|
||||
{
|
||||
name: "JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ import { StorageLike, watchDebounced } from "@vueuse/core"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
import { z } from "zod"
|
||||
import { CookieJarService } from "~/services/cookie-jar.service"
|
||||
import { watch } from "vue"
|
||||
|
||||
function checkAndMigrateOldSettings() {
|
||||
if (window.localStorage.getItem("selectedEnvIndex")) {
|
||||
@@ -182,6 +185,35 @@ function setupHistoryPersistence() {
|
||||
})
|
||||
}
|
||||
|
||||
const cookieSchema = z.record(z.array(z.string()))
|
||||
|
||||
function setupCookiesPersistence() {
|
||||
const cookieJarService = getService(CookieJarService)
|
||||
|
||||
try {
|
||||
const cookieData = JSON.parse(
|
||||
window.localStorage.getItem("cookieJar") || "{}"
|
||||
)
|
||||
|
||||
const parseResult = cookieSchema.safeParse(cookieData)
|
||||
|
||||
if (parseResult.success) {
|
||||
for (const domain in parseResult.data) {
|
||||
cookieJarService.bulkApplyCookiesToDomain(
|
||||
parseResult.data[domain],
|
||||
domain
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
watch(cookieJarService.cookieJar, (cookieJar) => {
|
||||
const data = JSON.stringify(Object.fromEntries(cookieJar.entries()))
|
||||
|
||||
window.localStorage.setItem("cookieJar", data)
|
||||
})
|
||||
}
|
||||
|
||||
function setupCollectionsPersistence() {
|
||||
const restCollectionData = JSON.parse(
|
||||
window.localStorage.getItem("collections") || "[]"
|
||||
@@ -382,6 +414,8 @@ export function setupLocalPersistence() {
|
||||
setupSocketIOPersistence()
|
||||
setupSSEPersistence()
|
||||
setupMQTTPersistence()
|
||||
|
||||
setupCookiesPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,12 +9,14 @@ import { AnalyticsPlatformDef } from "./analytics"
|
||||
import { InterceptorsPlatformDef } from "./interceptors"
|
||||
import { HoppModule } from "~/modules"
|
||||
import { InspectorsPlatformDef } from "./inspectors"
|
||||
import { IOPlatformDef } from "./io"
|
||||
|
||||
export type PlatformDef = {
|
||||
ui?: UIPlatformDef
|
||||
addedHoppModules?: HoppModule[]
|
||||
auth: AuthPlatformDef
|
||||
analytics?: AnalyticsPlatformDef
|
||||
io: IOPlatformDef
|
||||
sync: {
|
||||
environments: EnvironmentsPlatformDef
|
||||
collections: CollectionsPlatformDef
|
||||
@@ -27,6 +29,12 @@ export type PlatformDef = {
|
||||
platformFeatureFlags: {
|
||||
exportAsGIST: boolean
|
||||
hasTelemetry: boolean
|
||||
|
||||
/**
|
||||
* Whether the platform supports cookies (affects whether the cookies footer item is shown)
|
||||
* If a value is not given, then the value is assumed to be false
|
||||
*/
|
||||
cookiesEnabled?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
84
packages/hoppscotch-common/src/platform/io.ts
Normal file
84
packages/hoppscotch-common/src/platform/io.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Defines how to save a file to the user's filesystem.
|
||||
*/
|
||||
export type SaveFileWithDialogOptions = {
|
||||
/**
|
||||
* The data to be saved
|
||||
*/
|
||||
data: string | ArrayBuffer
|
||||
|
||||
/**
|
||||
* The suggested filename for the file. This name will be shown in the
|
||||
* save dialog by default when a save is initiated.
|
||||
*/
|
||||
suggestedFilename: string
|
||||
|
||||
/**
|
||||
* The content type mime type of the data to be saved.
|
||||
*
|
||||
* NOTE: The usage of this data might be platform dependent.
|
||||
* For example, this field is used in the web, but not in the desktop app.
|
||||
*/
|
||||
contentType: string
|
||||
|
||||
/**
|
||||
* Defines the filters (like in Windows, on the right side, where you can
|
||||
* select the file type) for the file dialog.
|
||||
*
|
||||
* NOTE: The usage of this data might be platform dependent.
|
||||
* For example, this field is used in the web, but not in the desktop app.
|
||||
*/
|
||||
filters?: Array<{
|
||||
/**
|
||||
* The name of the filter (in Windows, if the filter looks
|
||||
* like "Images (*.png, *.jpg)", the name would be "Images")
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* The array of extensions that are supported, without the dot.
|
||||
*/
|
||||
extensions: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export type SaveFileResponse =
|
||||
| {
|
||||
/**
|
||||
* The implementation was unable to determine the status of the save operation.
|
||||
* This cannot be considered a success or a failure and should be handled as an uncertainity.
|
||||
* The browser standard implementation (std) returns this value as there is no way to
|
||||
* check if the user downloaded the file or not.
|
||||
*/
|
||||
type: "unknown"
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The result is known and the user cancelled the save.
|
||||
*/
|
||||
type: "cancelled"
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The result is known and the user saved the file.
|
||||
*/
|
||||
type: "saved"
|
||||
|
||||
/**
|
||||
* The full path of where the file was saved
|
||||
*/
|
||||
path: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform definitions for how to handle IO operations.
|
||||
*/
|
||||
export type IOPlatformDef = {
|
||||
/**
|
||||
* Defines how to save a file to the user's filesystem.
|
||||
* The expected behaviour is for the browser to show a prompt to save the file.
|
||||
*/
|
||||
saveFileWithDialog: (
|
||||
opts: SaveFileWithDialogOptions
|
||||
) => Promise<SaveFileResponse>
|
||||
}
|
||||
37
packages/hoppscotch-common/src/platform/std/io.ts
Normal file
37
packages/hoppscotch-common/src/platform/std/io.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { IOPlatformDef } from "../io"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as S from "fp-ts/string"
|
||||
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
|
||||
|
||||
/**
|
||||
* Implementation for how to handle IO operations in the browser.
|
||||
*/
|
||||
export const browserIODef: IOPlatformDef = {
|
||||
saveFileWithDialog(opts) {
|
||||
const file = new Blob([opts.data], { type: opts.contentType })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
a.href = url
|
||||
a.download = pipe(
|
||||
url,
|
||||
S.split("/"),
|
||||
RNEA.last,
|
||||
S.split("#"),
|
||||
RNEA.head,
|
||||
S.split("?"),
|
||||
RNEA.head
|
||||
)
|
||||
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}, 1000)
|
||||
|
||||
// Browsers provide no way for us to know the save went successfully.
|
||||
return Promise.resolve({ type: "unknown" })
|
||||
},
|
||||
}
|
||||
@@ -72,6 +72,61 @@ describe("InterceptorService", () => {
|
||||
expect(service.currentInterceptorID.value).not.toEqual("unknown")
|
||||
})
|
||||
|
||||
it("currentInterceptor points to the instance of the currently selected interceptor", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const service = container.bind(InterceptorService)
|
||||
|
||||
const interceptor = {
|
||||
interceptorID: "test",
|
||||
name: () => "test interceptor",
|
||||
selectable: { type: "selectable" as const },
|
||||
runRequest: () => {
|
||||
throw new Error("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
service.registerInterceptor(interceptor)
|
||||
service.currentInterceptorID.value = "test"
|
||||
|
||||
expect(service.currentInterceptor.value).toBe(interceptor)
|
||||
})
|
||||
|
||||
it("currentInterceptor updates when the currentInterceptorID changes", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const service = container.bind(InterceptorService)
|
||||
|
||||
const interceptor = {
|
||||
interceptorID: "test",
|
||||
name: () => "test interceptor",
|
||||
selectable: { type: "selectable" as const },
|
||||
runRequest: () => {
|
||||
throw new Error("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
const interceptor_2 = {
|
||||
interceptorID: "test2",
|
||||
name: () => "test interceptor",
|
||||
selectable: { type: "selectable" as const },
|
||||
runRequest: () => {
|
||||
throw new Error("not implemented")
|
||||
},
|
||||
}
|
||||
|
||||
service.registerInterceptor(interceptor)
|
||||
service.registerInterceptor(interceptor_2)
|
||||
|
||||
service.currentInterceptorID.value = "test"
|
||||
|
||||
expect(service.currentInterceptor.value).toBe(interceptor)
|
||||
|
||||
service.currentInterceptorID.value = "test2"
|
||||
expect(service.currentInterceptor.value).not.toBe(interceptor)
|
||||
expect(service.currentInterceptor.value).toBe(interceptor_2)
|
||||
})
|
||||
|
||||
describe("registerInterceptor", () => {
|
||||
it("should register the interceptor", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Service } from "dioc"
|
||||
import { ref } from "vue"
|
||||
import { parseString as setCookieParse } from "set-cookie-parser-es"
|
||||
|
||||
export type CookieDef = {
|
||||
name: string
|
||||
value: string
|
||||
domain: string
|
||||
path: string
|
||||
expires: string
|
||||
}
|
||||
|
||||
export class CookieJarService extends Service {
|
||||
public static readonly ID = "COOKIE_JAR_SERVICE"
|
||||
|
||||
/**
|
||||
* The cookie jar that stores all relevant cookie info.
|
||||
* The keys correspond to the domain of the cookie.
|
||||
* The cookie strings are stored as an array of strings corresponding to the domain
|
||||
*/
|
||||
public cookieJar = ref(new Map<string, string[]>())
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public parseSetCookieString(setCookieString: string) {
|
||||
return setCookieParse(setCookieString)
|
||||
}
|
||||
|
||||
public bulkApplyCookiesToDomain(cookies: string[], domain: string) {
|
||||
const existingDomainEntries = this.cookieJar.value.get(domain) ?? []
|
||||
existingDomainEntries.push(...cookies)
|
||||
|
||||
this.cookieJar.value.set(domain, existingDomainEntries)
|
||||
}
|
||||
|
||||
public getCookiesForURL(url: URL) {
|
||||
const relevantDomains = Array.from(this.cookieJar.value.keys()).filter(
|
||||
(domain) => url.hostname.endsWith(domain)
|
||||
)
|
||||
|
||||
return relevantDomains
|
||||
.flatMap((domain) => {
|
||||
// Assemble the list of cookie entries from all the relevant domains
|
||||
|
||||
const cookieStrings = this.cookieJar.value.get(domain)! // We know not nullable from how we filter above
|
||||
|
||||
return cookieStrings.map((cookieString) =>
|
||||
this.parseSetCookieString(cookieString)
|
||||
)
|
||||
})
|
||||
.filter((cookie) => {
|
||||
// Perform the required checks on the cookies
|
||||
|
||||
const passesPathCheck = url.pathname.startsWith(cookie.path ?? "/")
|
||||
|
||||
const passesExpiresCheck = !cookie.expires
|
||||
? true
|
||||
: cookie.expires.getTime() >= new Date().getTime()
|
||||
|
||||
const passesSecureCheck = !cookie.secure
|
||||
? true
|
||||
: url.protocol === "https:"
|
||||
|
||||
return passesPathCheck && passesExpiresCheck && passesSecureCheck
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,12 @@ export type Interceptor<Err extends InterceptorError = InterceptorError> = {
|
||||
*/
|
||||
name: (t: ReturnType<typeof getI18n>) => MaybeRef<string>
|
||||
|
||||
/**
|
||||
* Defines whether the interceptor has support for cookies.
|
||||
* If this field is undefined, it is assumed as not supporting cookies.
|
||||
*/
|
||||
supportsCookies?: boolean
|
||||
|
||||
/**
|
||||
* Defines what to render in the Interceptor section of the Settings page.
|
||||
* Use this space to define interceptor specific settings.
|
||||
@@ -161,6 +167,16 @@ export class InterceptorService extends Service {
|
||||
Array.from(this.interceptors.values())
|
||||
)
|
||||
|
||||
/**
|
||||
* Gives an instance to the current interceptor.
|
||||
* NOTE: Do not update from here, this is only for reading.
|
||||
*/
|
||||
public currentInterceptor = computed(() => {
|
||||
if (this.currentInterceptorID.value === null) return null
|
||||
|
||||
return this.interceptors.get(this.currentInterceptorID.value)
|
||||
})
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user