Compare commits

...

11 Commits

Author SHA1 Message Date
Liyas Thomas
502da61b8b fix: ui styles 2023-12-07 14:43:22 +05:30
nivedin
f2777a9a75 chore: cleanup 2023-12-07 14:43:22 +05:30
nivedin
4bd3e89f89 refactor: update embed customize flow 2023-12-07 14:43:22 +05:30
nivedin
09e9601940 chore: add save option to modify sahred request 2023-12-07 14:43:22 +05:30
nivedin
fd4a5c626f chore: disable method and input 2023-12-07 14:43:22 +05:30
nivedin
67cfef82af chore: cleanup 2023-12-07 14:43:22 +05:30
nivedin
aa18249791 chore: use shared request properties for embed 2023-12-07 14:43:22 +05:30
nivedin
9d8fdb4d04 chore: add doc button if response is null 2023-12-07 14:43:22 +05:30
nivedin
fbca9b06c3 chore: add open app link 2023-12-07 14:43:22 +05:30
nivedin
bb0bf35164 feat: added embeds components wip 2023-12-07 14:43:22 +05:30
nivedin
5a35c098ec feat: mutations and small refactor in share req 2023-12-07 14:43:22 +05:30
23 changed files with 783 additions and 145 deletions

View File

@@ -96,6 +96,7 @@
"keyboard_shortcuts": "Keyboard shortcuts", "keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch", "name": "Hoppscotch",
"new_version_found": "New version found. Refresh to update.", "new_version_found": "New version found. Refresh to update.",
"open_in_hoppscotch": "Open in Hoppscotch",
"options": "Options", "options": "Options",
"proxy_privacy_policy": "Proxy privacy policy", "proxy_privacy_policy": "Proxy privacy policy",
"reload": "Reload", "reload": "Reload",
@@ -431,8 +432,9 @@
"close_unsaved_tab": "You have unsaved changes", "close_unsaved_tab": "You have unsaved changes",
"collections": "Collections", "collections": "Collections",
"confirm": "Confirm", "confirm": "Confirm",
"customize_request": "Customize Request",
"edit_request": "Edit Request", "edit_request": "Edit Request",
"share_request":"Share Request", "share_request": "Share Request",
"import_export": "Import / Export" "import_export": "Import / Export"
}, },
"mqtt": { "mqtt": {
@@ -621,29 +623,30 @@
"additional": "Additional Settings", "additional": "Additional Settings",
"verify_email": "Verify email" "verify_email": "Verify email"
}, },
"shared_requests":{ "shared_requests": {
"button":"Button", "button": "Button",
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.", "button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
"customize": "Customize", "customize": "Customize",
"creating_widget": "Creating widget", "creating_widget": "Creating widget",
"copy_html": "Copy HTML", "copy_html": "Copy HTML",
"copy_link": "Copy Link", "copy_link": "Copy Link",
"copy_markdown": "Copy Markdown", "copy_markdown": "Copy Markdown",
"deleted":"Shared request deleted", "deleted": "Shared request deleted",
"description": "Select a widget, you can change and customize this later", "description": "Select a widget, you can change and customize this later",
"embed":"Embed", "embed": "Embed",
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.", "embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
"link":"Link", "link": "Link",
"link_info": "Create a shareable link to share with anyone on the internet with view access.", "link_info": "Create a shareable link to share with anyone on the internet with view access.",
"not_found":"Shared request not found", "modified": "Shared request modified",
"not_found": "Shared request not found",
"open_new_tab": "Open in new tab", "open_new_tab": "Open in new tab",
"preview":"Preview", "preview": "Preview",
"run_in_hoppscotch":"Run in Hoppscotch", "run_in_hoppscotch": "Run in Hoppscotch",
"theme":{ "theme": {
"dark":"Dark", "dark": "Dark",
"light":"Light", "light": "Light",
"system" :"System", "system": "System",
"title":"Theme" "title": "Theme"
} }
}, },
"shortcut": { "shortcut": {
@@ -689,7 +692,7 @@
"save_to_collections": "Save to Collections", "save_to_collections": "Save to Collections",
"send_request": "Send Request", "send_request": "Send Request",
"show_code": "Generate code snippet", "show_code": "Generate code snippet",
"share_request":"Share Request", "share_request": "Share Request",
"title": "Request" "title": "Request"
}, },
"response": { "response": {

View File

@@ -61,6 +61,7 @@ declare module 'vue' {
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default'] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default'] CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default'] CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Embeds: typeof import('./components/embeds/index.vue')['default']
Environments: typeof import('./components/environments/index.vue')['default'] Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default'] EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default'] EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']

View File

@@ -0,0 +1,212 @@
<template>
<div class="flex flex-1 flex-col">
<header
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2"
>
<div class="flex flex-1 items-center justify-between space-x-2">
<HoppButtonSecondary
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('app.name')"
to="https://hoppscotch.io/"
blank
/>
<div class="flex">
<HoppSmartItem
:label="t('app.open_in_hoppscotch')"
:to="sharedRequestURL"
blank
/>
</div>
</div>
</header>
<div class="flex-1">
<div
class="flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
>
<div
class="min-w-52 flex flex-1 whitespace-nowrap rounded border border-divider"
>
<div class="relative flex">
<span
class="flex justify-center items-center w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
>
{{ tab.document.request.method }}
</span>
</div>
<div
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
>
<input
name="method"
:value="tab.document.request.endpoint"
class="flex-1 px-4 bg-primary"
disabled
/>
</div>
</div>
<div class="mt-2 flex sm:mt-0">
<HoppButtonPrimary
id="send"
:title="`${t(
'action.send'
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
class="min-w-20 flex-1"
@click="!loading ? newSendRequest() : cancelRequest()"
/>
<HoppButtonSecondary
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="t('request.save')"
filled
:icon="IconSave"
class="flex-1 rounded rounded-r-none"
blank
:to="sharedRequestURL"
/>
</div>
</div>
</div>
<HttpRequestOptions
v-model="tab.document.request"
v-model:option-tab="selectedOptionTab"
:properties="properties"
/>
<HttpResponse :document="tab.document" :is-embed="true" />
</div>
</template>
<script lang="ts" setup>
import { Ref } from "vue"
import { computed, useModel } from "vue"
import { ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import * as E from "fp-ts/Either"
import { useStreamSubscriber } from "~/composables/stream"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import IconSave from "~icons/lucide/save"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
modelTab: HoppTab<HoppRESTDocument>
properties: string[]
sharedRequestID: string
}>()
const tab = useModel(props, "modelTab")
const selectedOptionTab = ref(props.properties[0])
const requestCancelFunc: Ref<(() => void) | null> = ref(null)
const loading = ref(false)
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const sharedRequestURL = computed(() => {
return `${baseURL}/r/${props.sharedRequestID}`
})
const { subscribeToStream } = useStreamSubscriber()
const newSendRequest = async () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`)
return
}
ensureMethodInEndpoint()
loading.value = true
const [cancel, streamPromise] = runRESTRequest$(tab)
const streamResult = await streamPromise
requestCancelFunc.value = cancel
if (E.isRight(streamResult)) {
subscribeToStream(
streamResult.right,
(responseState) => {
if (loading.value) {
// Check exists because, loading can be set to false
// when cancelled
updateRESTResponse(responseState)
}
},
() => {
loading.value = false
},
() => {
// TODO: Change this any to a proper type
const result = (streamResult.right as any).value
if (
result.type === "network_fail" &&
result.error?.error === "NO_PW_EXT_HOOK"
) {
const errorResponse: HoppRESTResponse = {
type: "extension_error",
error: result.error.humanMessage.heading,
component: result.error.component,
req: result.req,
}
updateRESTResponse(errorResponse)
}
loading.value = false
}
)
} else {
loading.value = false
toast.error(`${t("error.script_fail")}`)
let error: Error
if (typeof streamResult.left === "string") {
error = { name: "RequestFailure", message: streamResult.left }
} else {
error = streamResult.left
}
updateRESTResponse({
type: "script_fail",
error,
})
}
}
const updateRESTResponse = (response: HoppRESTResponse | null) => {
tab.value.document.response = response
}
const newEndpoint = computed(() => {
return tab.value.document.request.endpoint
})
const ensureMethodInEndpoint = () => {
if (
!/^http[s]?:\/\//.test(newEndpoint.value) &&
!newEndpoint.value.startsWith("<<")
) {
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
tab.value.document.request.endpoint =
"http://" + tab.value.document.request.endpoint
} else {
tab.value.document.request.endpoint =
"https://" + tab.value.document.request.endpoint
}
}
}
const cancelRequest = () => {
loading.value = false
requestCancelFunc.value?.()
updateRESTResponse(null)
}
</script>

View File

@@ -5,13 +5,18 @@
render-inactive-tabs render-inactive-tabs
> >
<HoppSmartTab <HoppSmartTab
v-if="properties ? properties.includes('parameters') : true"
:id="'params'" :id="'params'"
:label="`${t('tab.parameters')}`" :label="`${t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`" :info="`${newActiveParamsCount$}`"
> >
<HttpParameters v-model="request.params" /> <HttpParameters v-model="request.params" />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'bodyParams'" :label="`${t('tab.body')}`"> <HoppSmartTab
v-if="properties ? properties.includes('body') : true"
:id="'bodyParams'"
:label="`${t('tab.body')}`"
>
<HttpBody <HttpBody
v-model:headers="request.headers" v-model:headers="request.headers"
v-model:body="request.body" v-model:body="request.body"
@@ -19,16 +24,22 @@
/> />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
v-if="properties ? properties.includes('headers') : true"
:id="'headers'" :id="'headers'"
:label="`${t('tab.headers')}`" :label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`" :info="`${newActiveHeadersCount$}`"
> >
<HttpHeaders v-model="request" @change-tab="changeOptionTab" /> <HttpHeaders v-model="request" @change-tab="changeOptionTab" />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`"> <HoppSmartTab
v-if="properties ? properties.includes('authorization') : true"
:id="'authorization'"
:label="`${t('tab.authorization')}`"
>
<HttpAuthorization v-model="request.auth" /> <HttpAuthorization v-model="request.auth" />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
v-if="properties ? properties.includes('preRequestScript') : true"
:id="'preRequestScript'" :id="'preRequestScript'"
:label="`${t('tab.pre_request_script')}`" :label="`${t('tab.pre_request_script')}`"
:indicator=" :indicator="
@@ -40,6 +51,7 @@
<HttpPreRequestScript v-model="request.preRequestScript" /> <HttpPreRequestScript v-model="request.preRequestScript" />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
v-if="properties ? properties.includes('tests') : true"
:id="'tests'" :id="'tests'"
:label="`${t('tab.tests')}`" :label="`${t('tab.tests')}`"
:indicator=" :indicator="
@@ -76,6 +88,7 @@ const props = withDefaults(
defineProps<{ defineProps<{
modelValue: HoppRESTRequest modelValue: HoppRESTRequest
optionTab: RESTOptionTabs optionTab: RESTOptionTabs
properties?: string[]
}>(), }>(),
{ {
optionTab: "params", optionTab: "params",

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="relative flex flex-1 flex-col"> <div class="relative flex flex-1 flex-col">
<HttpResponseMeta :response="doc.response" /> <HttpResponseMeta :response="doc.response" :is-embed="isEmbed" />
<LensesResponseBodyRenderer <LensesResponseBodyRenderer
v-if="!loading && hasResponse" v-if="!loading && hasResponse"
v-model:document="doc" v-model:document="doc"
@@ -15,6 +15,7 @@ import { HoppRESTDocument } from "~/helpers/rest/document"
const props = defineProps<{ const props = defineProps<{
document: HoppRESTDocument document: HoppRESTDocument
isEmbed: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{

View File

@@ -2,8 +2,20 @@
<div <div
class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-center overflow-auto overflow-x-auto whitespace-nowrap bg-primary p-4" class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-center overflow-auto overflow-x-auto whitespace-nowrap bg-primary p-4"
> >
<AppShortcutsPrompt v-if="response == null" class="flex-1" /> <AppShortcutsPrompt v-if="response == null && !isEmbed" class="flex-1" />
<div v-else class="flex flex-1 flex-col">
<div v-if="response == null && isEmbed">
<HoppButtonSecondary
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io/documentation/features/rest-api-testing#response"
:icon="IconExternalLink"
blank
outline
reverse
/>
</div>
<div v-else-if="response" class="flex flex-1 flex-col">
<div <div
v-if="response.type === 'loading'" v-if="response.type === 'loading'"
class="flex flex-col items-center justify-center" class="flex flex-col items-center justify-center"
@@ -105,6 +117,7 @@ import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection" import { InspectionService } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import IconExternalLink from "~icons/lucide/external-link"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
@@ -112,6 +125,7 @@ const tabs = useService(RESTTabService)
const props = defineProps<{ const props = defineProps<{
response: HoppRESTResponse | null | undefined response: HoppRESTResponse | null | undefined
isEmbed?: boolean
}>() }>()
/** /**

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="selectedWidget" v-if="selectedWidget"
class="divide-y divide-divider rounded border border-divider" class="border divide-y rounded divide-divider border-divider"
> >
<div v-if="loading" class="px-4 py-2"> <div v-if="loading" class="px-4 py-2">
{{ t("shared_requests.creating_widget") }} {{ t("shared_requests.creating_widget") }}
@@ -10,17 +10,17 @@
{{ t("shared_requests.description") }} {{ t("shared_requests.description") }}
</div> </div>
<div class="flex flex-col divide-y divide-divider"> <div class="flex flex-col divide-y divide-divider">
<div class="flex flex-col space-y-4 p-4"> <div class="flex flex-col p-4 space-y-4">
<div <div
v-for="widget in widgets" v-for="widget in widgets"
:key="widget.value" :key="widget.value"
class="flex cursor-pointer flex-col space-y-2 rounded border border-divider px-4 py-3 hover:bg-dividerLight" class="flex flex-col p-4 border rounded cursor-pointer border-divider hover:bg-dividerLight"
:class="{ :class="{
'!border-accentLight': selectedWidget.value === widget.value, '!border-accentLight': selectedWidget.value === widget.value,
}" }"
@click="selectedWidget = widget" @click="selectedWidget = widget"
> >
<span class="text-md font-bold"> <span class="mb-1 font-bold text-secondaryDark">
{{ widget.label }} {{ widget.label }}
</span> </span>
<span class="text-tiny"> <span class="text-tiny">
@@ -28,9 +28,13 @@
</span> </span>
</div> </div>
</div> </div>
<div class="flex flex-col divide-y divide-divider"> <div class="flex flex-col items-center justify-center p-4">
<div class="px-4 py-3">{{ t("shared_requests.preview") }}</div> <span
<div class="flex flex-col items-center justify-center px-4 py-10"> class="flex justify-center flex-1 mb-2 text-secondaryLight text-tiny"
>
{{ t("shared_requests.preview") }}
</span>
<div class="w-full">
<ShareTemplatesEmbeds <ShareTemplatesEmbeds
v-if="selectedWidget.value === 'embed'" v-if="selectedWidget.value === 'embed'"
:endpoint="request?.endpoint" :endpoint="request?.endpoint"
@@ -132,7 +136,7 @@ const embedOption = ref<EmbedOption>({
{ {
value: "authorization", value: "authorization",
label: t("tab.authorization"), label: t("tab.authorization"),
enabled: true, enabled: false,
}, },
], ],
theme: "system", theme: "system",

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="selectedWidget" v-if="selectedWidget"
class="divide-y divide-divider rounded border border-divider" class="border divide-y rounded divide-divider border-divider"
> >
<div v-if="loading" class="px-4 py-2"> <div v-if="loading" class="px-4 py-2">
{{ t("shared_requests.creating_widget") }} {{ t("shared_requests.creating_widget") }}
@@ -14,7 +14,7 @@
<span class="text-secondaryLight">{{ t("state.loading") }}</span> <span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div> </div>
<div v-else class="flex flex-col divide-y divide-divider"> <div v-else class="flex flex-col divide-y divide-divider">
<div class="flex flex-col space-y-4 p-4"> <div class="flex flex-col p-2 space-y-2">
<HoppSmartRadioGroup <HoppSmartRadioGroup
v-model="selectedWidget.value" v-model="selectedWidget.value"
:radios="widgets" :radios="widgets"
@@ -22,9 +22,9 @@
/> />
</div> </div>
<div class="flex flex-col divide-y divide-divider"> <div class="flex flex-col divide-y divide-divider">
<div class="flex items-center justify-center px-4 py-8"> <div class="flex items-center justify-center px-6 py-4">
<div v-if="selectedWidget.value === 'embed'" class="w-full flex-1"> <div v-if="selectedWidget.value === 'embed'" class="w-full">
<div class="flex flex-col pb-8"> <div class="flex flex-col pb-4">
<div <div
v-for="option in embedOptions.tabs" v-for="option in embedOptions.tabs"
:key="option.value" :key="option.value"
@@ -36,7 +36,8 @@
<HoppSmartCheckbox <HoppSmartCheckbox
:on="option.enabled" :on="option.enabled"
@change="removeEmbedOption(option.value)" @change="removeEmbedOption(option.value)"
/> >
</HoppSmartCheckbox>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span> <span>
@@ -49,13 +50,11 @@
theme="popover" theme="popover"
:on-shown="() => tippyActions!.focus()" :on-shown="() => tippyActions!.focus()"
> >
<span class="select-wrapper"> <HoppButtonSecondary
<HoppButtonSecondary class="!py-2 !px-0 capitalize"
class="ml-2 rounded-none pr-8 capitalize" :label="embedOptions.theme"
:label="embedOptions.theme" :icon="embedThemeIcon"
:icon="embedThemeIcon" />
/>
</span>
<template #content="{ hide }"> <template #content="{ hide }">
<div <div
ref="tippyActions" ref="tippyActions"
@@ -102,14 +101,20 @@
</div> </div>
</div> </div>
</div> </div>
<span
class="flex justify-center mb-2 text-secondaryLight text-tiny"
>
{{ t("shared_requests.preview") }}
</span>
<ShareTemplatesEmbeds <ShareTemplatesEmbeds
:endpoint="request?.endpoint" :endpoint="request?.endpoint"
:method="request?.method" :method="request?.method"
:model-value="embedOptions" :model-value="embedOptions"
/> />
<div class="flex items-center justify-center py-4"> <div class="flex items-center justify-center">
<HoppButtonSecondary <HoppButtonSecondary
:label="t('shared_requests.copy_html')" :label="t('shared_requests.copy_html')"
class="underline text-secondaryDark"
@click=" @click="
copyContent({ copyContent({
widget: 'embed', widget: 'embed',
@@ -126,12 +131,18 @@
<div <div
v-for="variant in buttonVariants" v-for="variant in buttonVariants"
:key="variant.id" :key="variant.id"
class="flex flex-col space-y-4" class="flex flex-col"
> >
<span
class="flex justify-center mb-2 text-secondaryLight text-tiny"
>
{{ t("shared_requests.preview") }}
</span>
<ShareTemplatesButton :img="variant.img" /> <ShareTemplatesButton :img="variant.img" />
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<HoppButtonSecondary <HoppButtonSecondary
:label="t('shared_requests.copy_html')" :label="t('shared_requests.copy_html')"
class="underline text-secondaryDark"
@click=" @click="
copyContent({ copyContent({
widget: 'button', widget: 'button',
@@ -142,6 +153,7 @@
/> />
<HoppButtonSecondary <HoppButtonSecondary
:label="t('shared_requests.copy_markdown')" :label="t('shared_requests.copy_markdown')"
class="underline text-secondaryDark"
@click=" @click="
copyContent({ copyContent({
widget: 'button', widget: 'button',
@@ -157,12 +169,17 @@
<div <div
v-for="variant in linkVariants" v-for="variant in linkVariants"
:key="variant.type" :key="variant.type"
class="flex flex-col items-center justify-center space-y-2" class="flex flex-col items-center justify-center"
> >
<span
class="flex justify-center mb-2 text-secondaryLight text-tiny"
>
{{ t("shared_requests.preview") }}
</span>
<ShareTemplatesLink :link="variant.link" :label="variant.label" /> <ShareTemplatesLink :link="variant.link" :label="variant.label" />
<HoppButtonSecondary <HoppButtonSecondary
:label="t(`shared_requests.copy_${variant.type}`)" :label="t(`shared_requests.copy_${variant.type}`)"
class="underline text-secondaryDark"
@click=" @click="
copyContent({ copyContent({
widget: 'link', widget: 'link',
@@ -205,6 +222,35 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
embedOptions: {
type: Object as PropType<EmbedOption>,
default: () => ({
selectedTab: "parameters",
tabs: [
{
value: "parameters",
label: "shared_requests.parameters",
enabled: true,
},
{
value: "body",
label: "shared_requests.body",
enabled: true,
},
{
value: "headers",
label: "shared_requests.headers",
enabled: true,
},
{
value: "authorization",
label: "shared_requests.authorization",
enabled: false,
},
],
theme: "system",
}),
},
}) })
const emit = defineEmits<{ const emit = defineEmits<{
@@ -220,6 +266,7 @@ const emit = defineEmits<{
}>() }>()
const selectedWidget = useVModel(props, "modelValue") const selectedWidget = useVModel(props, "modelValue")
const embedOptions = useVModel(props, "embedOptions")
type WidgetID = "embed" | "button" | "link" type WidgetID = "embed" | "button" | "link"
@@ -254,34 +301,6 @@ type EmbedOption = {
}[] }[]
theme: "light" | "dark" | "system" theme: "light" | "dark" | "system"
} }
const embedOptions = ref<EmbedOption>({
selectedTab: "parameters",
tabs: [
{
value: "parameters",
label: t("tab.parameters"),
enabled: true,
},
{
value: "body",
label: t("tab.body"),
enabled: true,
},
{
value: "headers",
label: t("tab.headers"),
enabled: true,
},
{
value: "authorization",
label: t("tab.authorization"),
enabled: true,
},
],
theme: "system",
})
const embedThemeIcon = computed(() => { const embedThemeIcon = computed(() => {
if (embedOptions.value.theme === "system") { if (embedOptions.value.theme === "system") {
return IconMonitor return IconMonitor
@@ -355,12 +374,7 @@ const linkVariants: LinkVariant[] = [
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh" const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const copyEmbed = () => { const copyEmbed = () => {
const options = embedOptions.value return `<iframe src="${baseURL}/e/${props.request?.id}' style='width: 100%; height: 500px; border: 0; border-radius: 4px; overflow: hidden;'></iframe>`
const enabledEmbedOptions = options.tabs
.filter((tab) => tab.enabled)
.map((tab) => tab.value)
.toString()
return `<iframe src="${baseURL}/e/${props.request?.id}/${enabledEmbedOptions}' style='width: 100%; height: 500px; border: 0; border-radius: 4px; overflow: hidden;'></iframe>`
} }
const copyButton = ( const copyButton = (
@@ -410,7 +424,10 @@ const copyContent = ({
} else { } else {
content = copyEmbed() content = copyEmbed()
} }
const copyContent = { sharedRequestID: props.request?.id, content } const copyContent = {
sharedRequestID: props.request?.id,
content,
}
emit("copy-shared-request", copyContent) emit("copy-shared-request", copyContent)
} }

View File

@@ -2,7 +2,9 @@
<HoppSmartModal <HoppSmartModal
v-if="show" v-if="show"
dialog dialog
:title="t('modal.share_request')" :title="
step === 1 ? t('modal.share_request') : t('modal.customize_request')
"
styles="sm:max-w-md" styles="sm:max-w-md"
@close="hideModal" @close="hideModal"
> >
@@ -21,6 +23,7 @@
<ShareCustomizeModal <ShareCustomizeModal
v-else-if="step === 2" v-else-if="step === 2"
v-model="selectedWidget" v-model="selectedWidget"
v-model:embed-options="embedOptions"
:request="request" :request="request"
:loading="loading" :loading="loading"
@copy-shared-request="copySharedRequest" @copy-shared-request="copySharedRequest"
@@ -28,19 +31,21 @@
</template> </template>
<template #footer> <template #footer>
<div v-if="step === 1" class="flex justify-end"> <div class="flex justify-start flex-1">
<HoppButtonPrimary <HoppButtonPrimary
v-if="step === 1"
:label="t('action.create')" :label="t('action.create')"
:loading="loading" :loading="loading"
@click="createSharedRequest" @click="createSharedRequest"
/> />
<HoppButtonSecondary <HoppButtonSecondary
:label="t('action.cancel')" :label="step === 1 ? t('action.cancel') : t('action.close')"
class="mr-2" class="ml-2"
filled
outline
@click="hideModal" @click="hideModal"
/> />
</div> </div>
<HoppButtonPrimary v-else :label="t('action.close')" @click="hideModal" />
</template> </template>
</HoppSmartModal> </HoppSmartModal>
</template> </template>
@@ -53,6 +58,18 @@ import { useI18n } from "~/composables/i18n"
const t = useI18n() const t = useI18n()
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
type EmbedOption = {
selectedTab: EmbedTabs
tabs: {
value: EmbedTabs
label: string
enabled: boolean
}[]
theme: "light" | "dark" | "system"
}
const props = defineProps({ const props = defineProps({
request: { request: {
type: Object as PropType<HoppRESTRequest | null>, type: Object as PropType<HoppRESTRequest | null>,
@@ -75,6 +92,35 @@ const props = defineProps({
type: Number, type: Number,
default: 1, default: 1,
}, },
embedOptions: {
type: Object as PropType<EmbedOption>,
default: () => ({
selectedTab: "parameters",
tabs: [
{
value: "parameters",
label: "shared_requests.parameters",
enabled: true,
},
{
value: "body",
label: "shared_requests.body",
enabled: true,
},
{
value: "headers",
label: "shared_requests.headers",
enabled: true,
},
{
value: "authorization",
label: "shared_requests.authorization",
enabled: false,
},
],
theme: "system",
}),
},
}) })
type WidgetID = "embed" | "button" | "link" type WidgetID = "embed" | "button" | "link"
@@ -86,6 +132,7 @@ type Widget = {
} }
const selectedWidget = useVModel(props, "modelValue") const selectedWidget = useVModel(props, "modelValue")
const embedOptions = useVModel(props, "embedOptions")
const emit = defineEmits<{ const emit = defineEmits<{
(e: "create-shared-request", request: HoppRESTRequest | null): void (e: "create-shared-request", request: HoppRESTRequest | null): void
@@ -94,7 +141,7 @@ const emit = defineEmits<{
(e: "update:step", value: number): void (e: "update:step", value: number): void
( (
e: "copy-shared-request", e: "copy-shared-request",
request: { payload: {
sharedRequestID: string | undefined sharedRequestID: string | undefined
content: string | undefined content: string | undefined
} }
@@ -105,11 +152,11 @@ const createSharedRequest = () => {
emit("create-shared-request", props.request as HoppRESTRequest) emit("create-shared-request", props.request as HoppRESTRequest)
} }
const copySharedRequest = (request: { const copySharedRequest = (payload: {
sharedRequestID: string | undefined sharedRequestID: string | undefined
content: string | undefined content: string | undefined
}) => { }) => {
emit("copy-shared-request", request) emit("copy-shared-request", payload)
} }
const hideModal = () => { const hideModal = () => {

View File

@@ -1,31 +1,31 @@
<template> <template>
<div <div
class="group flex items-stretch" class="flex items-stretch group"
@contextmenu.prevent="options!.tippy.show()" @contextmenu.prevent="options!.tippy.show()"
> >
<div <div
v-tippy="{ theme: 'tooltip', delay: [500, 20] }" v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center py-2" class="flex items-center justify-center flex-1 min-w-0 py-2 cursor-pointer pointer-events-auto"
:title="`${timeStamp}`" :title="`${timeStamp}`"
@click="openInNewTab" @click="openInNewTab"
> >
<span <span
class="pointer-events-none flex w-16 items-center justify-center truncate px-2" class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
:style="{ color: requestLabelColor }" :style="{ color: requestLabelColor }"
> >
<span class="truncate text-tiny font-semibold"> <span class="font-semibold truncate text-tiny">
{{ parseRequest.method }} {{ parseRequest.method }}
</span> </span>
</span> </span>
<span <span
class="pointer-events-none flex min-w-0 flex-1 items-center pr-2 transition group-hover:text-secondaryDark" class="flex items-center flex-1 min-w-0 pr-2 transition pointer-events-none group-hover:text-secondaryDark"
> >
<span class="flex-1 truncate"> <span class="flex-1 truncate">
{{ parseRequest.endpoint }} {{ parseRequest.endpoint }}
</span> </span>
</span> </span>
<span <span
class="flex-1 truncate border-l border-dividerDark px-2 text-secondaryLight group-hover:text-secondaryDark" class="flex px-2 truncate text-secondaryLight group-hover:text-secondaryDark"
> >
{{ parseRequest.name }} {{ parseRequest.name }}
</span> </span>
@@ -69,7 +69,7 @@
/> />
<HoppSmartItem <HoppSmartItem
ref="customizeAction" ref="customizeAction"
:icon="IconFileEdit" :icon="IconCustomize"
:label="`${t('shared_requests.customize')}`" :label="`${t('shared_requests.customize')}`"
:shortcut="['E']" :shortcut="['E']"
@click=" @click="
@@ -110,7 +110,7 @@ import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import { Shortcode } from "~/helpers/shortcode/Shortcode" import { Shortcode } from "~/helpers/shortcode/Shortcode"
import IconArrowUpRight from "~icons/lucide/arrow-up-right-square" import IconArrowUpRight from "~icons/lucide/arrow-up-right-square"
import IconMoreVertical from "~icons/lucide/more-vertical" import IconMoreVertical from "~icons/lucide/more-vertical"
import IconFileEdit from "~icons/lucide/file-edit" import IconCustomize from "~icons/lucide/settings-2"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import { shortDateTime } from "~/helpers/utils/date" import { shortDateTime } from "~/helpers/utils/date"
@@ -121,7 +121,12 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "customize-shared-request", request: HoppRESTRequest, id: string): void (
e: "customize-shared-request",
request: HoppRESTRequest,
id: string,
embedProperties?: string | null
): void
(e: "delete-shared-request", codeID: string): void (e: "delete-shared-request", codeID: string): void
(e: "open-new-tab", request: HoppRESTRequest): void (e: "open-new-tab", request: HoppRESTRequest): void
}>() }>()
@@ -130,6 +135,7 @@ const tippyActions = ref<TippyComponent | null>(null)
const openInNewTabAction = ref<HTMLButtonElement | null>(null) const openInNewTabAction = ref<HTMLButtonElement | null>(null)
const customizeAction = ref<HTMLButtonElement | null>(null) const customizeAction = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null) const deleteAction = ref<HTMLButtonElement | null>(null)
const options = ref<any | null>(null)
const parseRequest = computed(() => const parseRequest = computed(() =>
pipe(props.request.request, JSON.parse, translateToNewRequest) pipe(props.request.request, JSON.parse, translateToNewRequest)
@@ -144,7 +150,13 @@ const openInNewTab = () => {
} }
const customizeSharedRequest = () => { const customizeSharedRequest = () => {
emit("customize-shared-request", parseRequest.value, props.request.id) const embedProperties = props.request.properties
emit(
"customize-shared-request",
parseRequest.value,
props.request.id,
embedProperties
)
} }
const deleteSharedRequest = () => { const deleteSharedRequest = () => {

View File

@@ -80,11 +80,12 @@
/> />
<ShareModal <ShareModal
v-model="selectedWidget" v-model="selectedWidget"
v-model:embed-options="embedOptions"
:step="step"
:request="requestToShare" :request="requestToShare"
:show="showShareRequestModal" :show="showShareRequestModal"
:loading="shareRequestCreatingLoading" :loading="shareRequestCreatingLoading"
:step="step" @hide-modal="displayCustomizeRequestModal(false, null)"
@hide-modal="displayCustomizeRequestModal(false)"
@copy-shared-request="copySharedRequest" @copy-shared-request="copySharedRequest"
@create-shared-request="createSharedRequest" @create-shared-request="createSharedRequest"
/> />
@@ -105,6 +106,7 @@ import * as TE from "fp-ts/TaskEither"
import { import {
deleteShortcode as backendDeleteShortcode, deleteShortcode as backendDeleteShortcode,
createShortcode, createShortcode,
updateEmbedProperties,
} from "~/helpers/backend/mutations/Shortcode" } from "~/helpers/backend/mutations/Shortcode"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
@@ -114,6 +116,7 @@ import { copyToClipboard } from "~/helpers/utils/clipboard"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { watch } from "vue"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
@@ -130,6 +133,70 @@ const shareRequestCreatingLoading = ref(false)
const requestToShare = ref<HoppRESTRequest | null>(null) const requestToShare = ref<HoppRESTRequest | null>(null)
const embedOptions = ref<EmbedOption>({
selectedTab: "parameters",
tabs: [
{
value: "parameters",
label: t("tab.parameters"),
enabled: false,
},
{
value: "body",
label: t("tab.body"),
enabled: false,
},
{
value: "headers",
label: t("tab.headers"),
enabled: false,
},
{
value: "authorization",
label: t("tab.authorization"),
enabled: false,
},
],
theme: "system",
})
const updateEmbedProperty = async (
shareRequestID: string,
properties: string
) => {
const customizeEmbedResult = await updateEmbedProperties(
shareRequestID,
properties
)()
if (E.isLeft(customizeEmbedResult)) {
toast.error(`${customizeEmbedResult.left.error}`)
toast.error(t("error.something_went_wrong"))
}
}
watch(
() => embedOptions.value,
() => {
if (
requestToShare.value &&
requestToShare.value.id &&
showShareRequestModal.value
) {
if (selectedWidget.value.value === "embed") {
const properties = {
options: embedOptions.value.tabs
.filter((tab) => tab.enabled)
.map((tab) => tab.value),
theme: embedOptions.value.theme,
}
updateEmbedProperty(requestToShare.value.id, JSON.stringify(properties))
}
}
},
{ deep: true }
)
const restTab = useService(RESTTabService) const restTab = useService(RESTTabService)
const currentUser = useReadonlyStream( const currentUser = useReadonlyStream(
@@ -139,6 +206,18 @@ const currentUser = useReadonlyStream(
const step = ref(1) const step = ref(1)
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
type EmbedOption = {
selectedTab: EmbedTabs
tabs: {
value: EmbedTabs
label: string
enabled: boolean
}[]
theme: "light" | "dark" | "system"
}
type WidgetID = "embed" | "button" | "link" type WidgetID = "embed" | "button" | "link"
type Widget = { type Widget = {
@@ -218,15 +297,73 @@ const displayShareRequestModal = (show: boolean) => {
showShareRequestModal.value = show showShareRequestModal.value = show
step.value = 1 step.value = 1
} }
const displayCustomizeRequestModal = (show: boolean) => {
const displayCustomizeRequestModal = (
show: boolean,
embedProperties?: string | null
) => {
showShareRequestModal.value = show showShareRequestModal.value = show
step.value = 2 step.value = 2
if (!embedProperties) {
selectedWidget.value = {
value: "button",
label: t("shared_requests.button"),
info: t("shared_requests.button_info"),
}
embedOptions.value = {
selectedTab: "parameters",
tabs: [
{
value: "parameters",
label: t("tab.parameters"),
enabled: false,
},
{
value: "body",
label: t("tab.body"),
enabled: false,
},
{
value: "headers",
label: t("tab.headers"),
enabled: false,
},
{
value: "authorization",
label: t("tab.authorization"),
enabled: false,
},
],
theme: "system",
}
} else {
const parsedEmbedProperties = JSON.parse(embedProperties)
embedOptions.value = {
selectedTab: parsedEmbedProperties.options[0],
tabs: embedOptions.value.tabs.map((tab) => {
return {
...tab,
enabled: parsedEmbedProperties.options.includes(tab.value),
}
}),
theme: parsedEmbedProperties.theme,
}
}
} }
const createSharedRequest = async (request: HoppRESTRequest | null) => { const createSharedRequest = async (request: HoppRESTRequest | null) => {
if (request && selectedWidget.value) { if (request && selectedWidget.value) {
const properties = {
options: ["parameters", "body", "headers"],
theme: "system",
}
shareRequestCreatingLoading.value = true shareRequestCreatingLoading.value = true
const sharedRequestResult = await createShortcode(request)() const sharedRequestResult = await createShortcode(
request,
selectedWidget.value.value === "embed"
? JSON.stringify(properties)
: undefined
)()
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_SHORTCODE_CREATED", type: "HOPP_SHORTCODE_CREATED",
@@ -243,6 +380,23 @@ const createSharedRequest = async (request: HoppRESTRequest | null) => {
id: sharedRequestResult.right.createShortcode.id, id: sharedRequestResult.right.createShortcode.id,
} }
step.value = 2 step.value = 2
if (sharedRequestResult.right.createShortcode.properties) {
const parsedEmbedProperties = JSON.parse(
sharedRequestResult.right.createShortcode.properties
)
embedOptions.value = {
selectedTab: parsedEmbedProperties.options[0],
tabs: embedOptions.value.tabs.map((tab) => {
return {
...tab,
enabled: parsedEmbedProperties.options.includes(tab.value),
}
}),
theme: parsedEmbedProperties.theme,
}
}
} }
} }
} }
@@ -250,21 +404,22 @@ const createSharedRequest = async (request: HoppRESTRequest | null) => {
const customizeSharedRequest = ( const customizeSharedRequest = (
request: HoppRESTRequest, request: HoppRESTRequest,
shredRequestID: string shredRequestID: string,
embedProperties?: string | null
) => { ) => {
requestToShare.value = { requestToShare.value = {
...request, ...request,
id: shredRequestID, id: shredRequestID,
} }
displayCustomizeRequestModal(true) displayCustomizeRequestModal(true, embedProperties)
} }
const copySharedRequest = (request: { const copySharedRequest = (payload: {
sharedRequestID: string | undefined sharedRequestID: string | undefined
content: string | undefined content: string | undefined
}) => { }) => {
if (request.content) { if (payload.content) {
copyToClipboard(request.content) copyToClipboard(payload.content)
toast.success(t("state.copied_to_clipboard")) toast.success(t("state.copied_to_clipboard"))
} }
} }

View File

@@ -1,10 +1,6 @@
<template> <template>
<div <div class="flex flex-col items-center p-4 border rounded border-dividerDark">
class="flex items-center justify-center rounded border border-dotted border-dividerDark p-5" <img :src="img" :alt="t('shared_requests.run_in_hoppscotch')" />
>
<a href="/" target="_blank">
<img :src="img" :alt="t('shared_requests.run_in_hoppscotch')" />
</a>
</div> </div>
</template> </template>

View File

@@ -1,32 +1,30 @@
<template> <template>
<div <div
class="flex flex-col rounded border border-dotted border-divider p-5" class="flex flex-col p-4 border rounded border-dividerDark"
:class="{ :class="{
'bg-accentContrast': isEmbedThemeLight, 'bg-accentContrast': isEmbedThemeLight,
}" }"
> >
<div <div
class="flex items-stretch space-x-4 rounded border-divider" class="flex items-stretch space-x-2"
:class="{ :class="{
'bg-accentContrast': isEmbedThemeLight, 'bg-accentContrast': isEmbedThemeLight,
}" }"
> >
<span <span class="flex items-center min-w-0 border rounded border-divider">
class="flex max-w-[4rem] items-center justify-center rounded border border-divider px-1 py-2 text-tiny"
:class="{
'!border-dividerLight bg-accentContrast text-primary':
isEmbedThemeLight,
}"
>
<span class="truncate">
{{ method }}
</span>
</span>
<span
class="flex max-w-46 items-center rounded border border-divider p-2"
>
<span <span
class="min-w-0 truncate" class="flex max-w-[4rem] rounded-l h-full items-center justify-center border-r border-divider text-tiny"
:class="{
'!border-dividerLight bg-accentContrast text-primary':
isEmbedThemeLight,
}"
>
<span class="px-3 truncate">
{{ method }}
</span>
</span>
<span
class="px-3 truncate"
:class="{ :class="{
'text-primary': isEmbedThemeLight, 'text-primary': isEmbedThemeLight,
}" }"
@@ -35,7 +33,7 @@
</span> </span>
</span> </span>
<button <button
class="flex items-center justify-center rounded border border-dividerDark bg-primaryDark px-3 py-2 font-semibold text-secondary" class="flex items-center justify-center flex-shrink-0 px-3 py-2 font-semibold border rounded border-dividerDark bg-primaryDark text-secondary"
:class="{ :class="{
'!bg-accentContrast text-primaryLight': isEmbedThemeLight, '!bg-accentContrast text-primaryLight': isEmbedThemeLight,
}" }"
@@ -44,10 +42,10 @@
</button> </button>
</div> </div>
<div <div
class="flex border-divider" class="flex"
:class="{ :class="{
'bg-accentContrast text-primary': isEmbedThemeLight, 'bg-accentContrast text-primary': isEmbedThemeLight,
'border-b pt-2 ': !noActiveTab, 'border-b border-divider pt-2': !noActiveTab,
}" }"
> >
<span <span
@@ -57,7 +55,8 @@
class="px-2 py-2" class="px-2 py-2"
:class="{ :class="{
'border-b border-dividerDark': 'border-b border-dividerDark':
embedOptions.selectedTab === option.value, embedOptions.tabs.filter((tab) => tab.enabled)[0]?.value ===
option.value,
}" }"
> >
{{ option.label }} {{ option.label }}

View File

@@ -1,7 +1,5 @@
<template> <template>
<div <div class="flex flex-col items-center p-4 border rounded border-dividerDark">
class="flex items-center justify-center rounded border border-dotted border-dividerDark p-5"
>
<span <span
:class="{ :class="{
'border-b border-secondary': label, 'border-b border-secondary': label,

View File

@@ -1,5 +1,5 @@
mutation CreateShortcode($request: String!) { mutation CreateShortcode($request: String!, $properties: String) {
createShortcode(request: $request) { createShortcode(request: $request, properties: $properties) {
id id
request request
createdOn createdOn

View File

@@ -0,0 +1,8 @@
mutation UpdateEmbedProperties($code: ID!, $properties: String!) {
updateEmbedProperties(code: $code, properties: $properties) {
id
request
properties
createdOn
}
}

View File

@@ -2,5 +2,6 @@ query ResolveShortcode($code: ID!) {
shortcode(code: $code) { shortcode(code: $code) {
id id
request request
properties
} }
} }

View File

@@ -0,0 +1,8 @@
subscription ShortcodeUpdated {
myShortcodesUpdated {
id
request
createdOn
properties
}
}

View File

@@ -7,15 +7,22 @@ import {
DeleteShortcodeDocument, DeleteShortcodeDocument,
DeleteShortcodeMutation, DeleteShortcodeMutation,
DeleteShortcodeMutationVariables, DeleteShortcodeMutationVariables,
UpdateEmbedPropertiesDocument,
UpdateEmbedPropertiesMutation,
UpdateEmbedPropertiesMutationVariables,
} from "../graphql" } from "../graphql"
type DeleteShortcodeErrors = "shortcode/not_found" type DeleteShortcodeErrors = "shortcode/not_found"
export const createShortcode = (request: HoppRESTRequest) => export const createShortcode = (
request: HoppRESTRequest,
properties?: string
) =>
runMutation<CreateShortcodeMutation, CreateShortcodeMutationVariables, "">( runMutation<CreateShortcodeMutation, CreateShortcodeMutationVariables, "">(
CreateShortcodeDocument, CreateShortcodeDocument,
{ {
request: JSON.stringify(request), request: JSON.stringify(request),
properties,
} }
) )
@@ -27,3 +34,13 @@ export const deleteShortcode = (code: string) =>
>(DeleteShortcodeDocument, { >(DeleteShortcodeDocument, {
code, code,
}) })
export const updateEmbedProperties = (code: string, properties: string) =>
runMutation<
UpdateEmbedPropertiesMutation,
UpdateEmbedPropertiesMutationVariables,
""
>(UpdateEmbedPropertiesDocument, {
code,
properties,
})

View File

@@ -4,6 +4,6 @@
export interface Shortcode { export interface Shortcode {
id: string id: string
request: string request: string
properties?: string | null | undefined properties?: string | null
createdOn: Date createdOn: Date
} }

View File

@@ -11,6 +11,7 @@ import {
GetUserShortcodesDocument, GetUserShortcodesDocument,
ShortcodeCreatedDocument, ShortcodeCreatedDocument,
ShortcodeDeletedDocument, ShortcodeDeletedDocument,
ShortcodeUpdatedDocument,
} from "../backend/graphql" } from "../backend/graphql"
import { BACKEND_PAGE_SIZE } from "../backend/helpers" import { BACKEND_PAGE_SIZE } from "../backend/helpers"
import { Shortcode } from "./Shortcode" import { Shortcode } from "./Shortcode"
@@ -25,9 +26,11 @@ export default class ShortcodeListAdapter {
private shortcodeCreated: Subscription | null private shortcodeCreated: Subscription | null
private shortcodeRevoked: Subscription | null private shortcodeRevoked: Subscription | null
private shortcodeUpdated: Subscription | null
private shortcodeCreatedSub: WSubscription | null private shortcodeCreatedSub: WSubscription | null
private shortcodeRevokedSub: WSubscription | null private shortcodeRevokedSub: WSubscription | null
private shortcodeUpdatedSub: WSubscription | null
constructor(deferInit = false) { constructor(deferInit = false) {
this.error$ = new BehaviorSubject<GQLError<string> | null>(null) this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
@@ -39,8 +42,10 @@ export default class ShortcodeListAdapter {
this.isDispose = true this.isDispose = true
this.shortcodeCreated = null this.shortcodeCreated = null
this.shortcodeRevoked = null this.shortcodeRevoked = null
this.shortcodeUpdated = null
this.shortcodeCreatedSub = null this.shortcodeCreatedSub = null
this.shortcodeRevokedSub = null this.shortcodeRevokedSub = null
this.shortcodeUpdatedSub = null
if (!deferInit) this.initialize() if (!deferInit) this.initialize()
} }
@@ -48,8 +53,10 @@ export default class ShortcodeListAdapter {
unsubscribeSubscriptions() { unsubscribeSubscriptions() {
this.shortcodeCreated?.unsubscribe() this.shortcodeCreated?.unsubscribe()
this.shortcodeRevoked?.unsubscribe() this.shortcodeRevoked?.unsubscribe()
this.shortcodeUpdated?.unsubscribe()
this.shortcodeCreatedSub?.unsubscribe() this.shortcodeCreatedSub?.unsubscribe()
this.shortcodeRevokedSub?.unsubscribe() this.shortcodeRevokedSub?.unsubscribe()
this.shortcodeUpdatedSub?.unsubscribe()
} }
initialize() { initialize() {
@@ -137,6 +144,14 @@ export default class ShortcodeListAdapter {
this.shortcodes$.next(newShortcode) this.shortcodes$.next(newShortcode)
} }
private updateSharedRequest(shortcode: Shortcode) {
const newShortcode = this.shortcodes$.value.map((oldShortcode) =>
oldShortcode.id === shortcode.id ? shortcode : oldShortcode
)
this.shortcodes$.next(newShortcode)
}
private registerSubscriptions() { private registerSubscriptions() {
const [shortcodeCreated$, shortcodeCreatedSub] = runAuthOnlyGQLSubscription( const [shortcodeCreated$, shortcodeCreatedSub] = runAuthOnlyGQLSubscription(
{ {
@@ -169,5 +184,21 @@ export default class ShortcodeListAdapter {
this.deleteSharedRequest(result.right.myShortcodesRevoked.id) this.deleteSharedRequest(result.right.myShortcodesRevoked.id)
}) })
const [shortcodeUpdated$, shortcodeUpdatedSub] = runAuthOnlyGQLSubscription(
{
query: ShortcodeUpdatedDocument,
}
)
this.shortcodeUpdatedSub = shortcodeUpdatedSub
this.shortcodeUpdated = shortcodeUpdated$.subscribe((result) => {
if (E.isLeft(result)) {
console.error(result.left)
throw new Error(`Shortcode Update Error ${result.left}`)
}
this.updateSharedRequest(result.right.myShortcodesUpdated)
})
} }
} }

View File

@@ -1,7 +1,107 @@
<template> <template>
<div class="flex flex-col items-center justify-between p-8"> <div class="flex flex-col flex-1 w-full">
Temporary page for Embed till the feature is ready <Embeds
v-if="tab"
v-model:modelTab="tab"
:properties="properties"
:shared-request-i-d="sharedRequestID"
/>
</div> </div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import { ref } from "vue"
import { watch } from "vue"
import { useRoute } from "vue-router"
import { useGQLQuery } from "~/composables/graphql"
import {
ResolveShortcodeDocument,
ResolveShortcodeQuery,
ResolveShortcodeQueryVariables,
} from "~/helpers/backend/graphql"
import * as E from "fp-ts/Either"
import { onMounted } from "vue"
import {
getDefaultRESTRequest,
safelyExtractRESTRequest,
} from "@hoppscotch/data"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { applySetting } from "~/newstore/settings"
const route = useRoute()
const sharedRequestID = ref("")
const invalidLink = ref(false)
const properties = ref([])
const sharedRequestDetails = useGQLQuery<
ResolveShortcodeQuery,
ResolveShortcodeQueryVariables,
""
>({
query: ResolveShortcodeDocument,
variables: {
code: route.params.id.toString(),
},
})
const tab = ref<HoppTab<HoppRESTDocument>>({
id: "0",
document: {
request: getDefaultRESTRequest(),
response: null,
isDirty: false,
},
})
watch(
() => sharedRequestDetails.data,
() => {
if (sharedRequestDetails.loading) return
const data = sharedRequestDetails.data
if (E.isRight(data)) {
if (!data.right.shortcode?.request) {
invalidLink.value = true
return
}
const request: unknown = JSON.parse(
data.right.shortcode?.request as string
)
tab.value.document.request = safelyExtractRESTRequest(
request,
getDefaultRESTRequest()
)
if (data.right.shortcode && data.right.shortcode.properties) {
const parsedProperties = JSON.parse(data.right.shortcode.properties)
if (parsedProperties.theme === "dark") {
applySetting("BG_COLOR", "dark")
} else if (parsedProperties.theme === "light") {
applySetting("BG_COLOR", "light")
} else if (parsedProperties.theme === "auto") {
applySetting("BG_COLOR", "system")
}
properties.value = parsedProperties.options
}
}
}
)
onMounted(() => {
if (typeof route.params.id === "string") {
sharedRequestID.value = route.params.id
sharedRequestDetails.execute()
}
invalidLink.value = !sharedRequestID.value
})
</script>
<route lang="yaml">
meta:
layout: empty
</route>

View File

@@ -75,6 +75,7 @@ const emit = defineEmits<{
@apply w-4; @apply w-4;
@apply mr-2; @apply mr-2;
@apply transition; @apply transition;
@apply empty:mr-0;
content: ""; content: "";
} }