feat: shared request (#3486)

Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
Nivedin
2023-12-04 22:51:18 +05:30
committed by GitHub
parent 259cd48dbb
commit 2528bbb92f
35 changed files with 1525 additions and 517 deletions

View File

@@ -222,6 +222,12 @@
requestIndex: pathToIndex(node.id),
})
"
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
request: node.data.data.data,
})
"
@drag-request="
dragRequest($event, {
folderPath: node.data.data.parentIndex,
@@ -460,6 +466,12 @@ const emit = defineEmits<{
isActive: boolean
}
): void
(
event: "share-request",
payload: {
request: HoppRESTRequest
}
): void
(
event: "drop-request",
payload: {

View File

@@ -94,6 +94,7 @@
@keyup.e="edit?.$el.click()"
@keyup.d="duplicate?.$el.click()"
@keyup.delete="deleteAction?.$el.click()"
@keyup.s="shareAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
@@ -133,6 +134,18 @@
}
"
/>
<HoppSmartItem
ref="shareAction"
:icon="IconShare2"
:label="t('action.share')"
:shortcut="['S']"
@click="
() => {
emit('share-request')
hide()
}
"
/>
</div>
</template>
</tippy>
@@ -162,6 +175,7 @@ import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconShare2 from "~icons/lucide/share-2"
import { ref, PropType, watch, computed } from "vue"
import { HoppRESTRequest } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
@@ -240,6 +254,7 @@ const emit = defineEmits<{
(event: "duplicate-request"): void
(event: "remove-request"): void
(event: "select-request"): void
(event: "share-request"): void
(event: "drag-request", payload: DataTransfer): void
(event: "update-request-order", payload: DataTransfer): void
(event: "update-last-request-order", payload: DataTransfer): void
@@ -250,6 +265,7 @@ const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null)
const duplicate = ref<HTMLButtonElement | null>(null)
const shareAction = ref<HTMLButtonElement | null>(null)
const dragging = ref(false)
const ordering = ref(false)

View File

@@ -240,6 +240,12 @@
requestIndex: node.data.data.data.id,
})
"
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
request: node.data.data.data.request,
})
"
@drag-request="
dragRequest($event, {
folderPath: node.data.data.parentIndex,
@@ -473,6 +479,12 @@ const emit = defineEmits<{
folderPath?: string | undefined
}
): void
(
event: "share-request",
payload: {
request: HoppRESTRequest
}
): void
(
event: "drop-request",
payload: {

View File

@@ -41,6 +41,7 @@
@export-data="exportData"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
@share-request="shareRequest"
@drop-collection="dropCollection"
@update-request-order="updateRequestOrder"
@update-collection-order="updateCollectionOrder"
@@ -71,6 +72,7 @@
@export-data="exportData"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
@share-request="shareRequest"
@edit-request="editRequest"
@duplicate-request="duplicateRequest"
@remove-request="removeRequest"
@@ -229,7 +231,7 @@ import {
resetTeamRequestsContext,
} from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler } from "~/helpers/actions"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
@@ -2012,6 +2014,17 @@ const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
)()
}
const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
if (currentUser.value) {
// opens the share request modal
invokeAction("share.request", {
request,
})
} else {
invokeAction("modals.login.toggle")
}
}
const resolveConfirmModal = (title: string | null) => {
if (title === `${t("confirm.remove_collection")}`) onRemoveCollection()
else if (title === `${t("confirm.remove_request")}`) onRemoveRequest()

View File

@@ -42,7 +42,7 @@
<div class="min-w-18" />
<HoppSmartCheckbox
:on="replaceWithVariable"
title="t('environment.replace_with_variable'))"
:title="t('environment.replace_with_variable')"
@change="replaceWithVariable = !replaceWithVariable"
/>
<label for="replaceWithVariable">

View File

@@ -179,20 +179,16 @@
/>
<HoppSmartItem
ref="copyRequestAction"
:label="shareButtonText"
:icon="copyLinkIcon"
:label="t('request.share_request')"
:icon="IconShare2"
:loading="fetchingShareLink"
@click="
() => {
copyRequest()
shareRequest()
hide()
}
"
/>
<HoppSmartItem
:icon="IconLink2"
:label="`${t('request.view_my_links')}`"
to="/profile"
/>
<hr />
<HoppSmartItem
ref="saveRequestAction"
@@ -236,25 +232,20 @@ import { useI18n } from "@composables/i18n"
import { useSetting } from "@composables/settings"
import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { refAutoReset, useVModel } from "@vueuse/core"
import { useVModel } from "@vueuse/core"
import * as E from "fp-ts/Either"
import { Ref, computed, onBeforeUnmount, ref } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { editRESTRequest } from "~/newstore/collections"
import IconCheck from "~icons/lucide/check"
import IconChevronDown from "~icons/lucide/chevron-down"
import IconCode2 from "~icons/lucide/code-2"
import IconCopy from "~icons/lucide/copy"
import IconFileCode from "~icons/lucide/file-code"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconLink2 from "~icons/lucide/link-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save"
import IconShare2 from "~icons/lucide/share-2"
@@ -309,8 +300,6 @@ const showCurlImportModal = ref(false)
const showCodegenModal = ref(false)
const showSaveRequestModal = ref(false)
const hasNavigatorShare = !!navigator.share
// Template refs
const methodTippyActions = ref<any | null>(null)
const sendTippyActions = ref<any | null>(null)
@@ -453,62 +442,20 @@ const updateRESTResponse = (response: HoppRESTResponse | null) => {
tab.value.document.response = response
}
const copyLinkIcon = refAutoReset<
typeof IconShare2 | typeof IconCopy | typeof IconCheck
>(hasNavigatorShare ? IconShare2 : IconCopy, 1000)
const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const shareLink = ref<string | null>("")
const fetchingShareLink = ref(false)
const shareButtonText = computed(() => {
if (shareLink.value) {
return shareLink.value
} else if (fetchingShareLink.value) {
return t("state.loading")
} else {
return t("request.copy_link")
}
})
const copyRequest = async () => {
if (shareLink.value) {
copyShareLink(shareLink.value)
} else {
shareLink.value = ""
fetchingShareLink.value = true
const shortcodeResult = await createShortcode(tab.value.document.request)()
platform.analytics?.logEvent({
type: "HOPP_SHORTCODE_CREATED",
})
if (E.isLeft(shortcodeResult)) {
toast.error(`${shortcodeResult.left.error}`)
shareLink.value = `${t("error.something_went_wrong")}`
} else if (E.isRight(shortcodeResult)) {
shareLink.value = `/${shortcodeResult.right.createShortcode.id}`
copyShareLink(shareLink.value)
}
fetchingShareLink.value = false
}
}
const copyShareLink = (shareLink: string) => {
const link = `${
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
}/r${shareLink}`
if (navigator.share) {
const time = new Date().toLocaleTimeString()
const date = new Date().toLocaleDateString()
navigator.share({
title: "Hoppscotch",
text: `Hoppscotch • Open source API development ecosystem at ${time} on ${date}`,
url: link,
const shareRequest = () => {
if (currentUser.value) {
invokeAction("share.request", {
request: tab.value.document.request,
})
} else {
copyLinkIcon.value = IconCheck
copyToClipboard(link)
toast.success(`${t("state.copied_to_clipboard")}`)
invokeAction("modals.login.toggle")
}
}
@@ -611,7 +558,7 @@ defineActionHandler("request.send-cancel", () => {
else cancelRequest()
})
defineActionHandler("request.reset", clearContent)
defineActionHandler("request.copy-link", copyRequest)
defineActionHandler("request.share-request", shareRequest)
defineActionHandler("request.method.next", cycleDownMethod)
defineActionHandler("request.method.prev", cycleUpMethod)
defineActionHandler("request.save", saveRequest)

View File

@@ -26,6 +26,13 @@
>
<History :page="'rest'" />
</HoppSmartTab>
<HoppSmartTab
:id="'share-request'"
:icon="IconShare2"
:label="`${t('tab.shared_requests')}`"
>
<Share />
</HoppSmartTab>
</HoppSmartTabs>
</template>
@@ -33,6 +40,7 @@
import IconClock from "~icons/lucide/clock"
import IconLayers from "~icons/lucide/layers"
import IconFolder from "~icons/lucide/folder"
import IconShare2 from "~icons/lucide/share-2"
import { ref } from "vue"
import { useI18n } from "@composables/i18n"

View File

@@ -1,122 +0,0 @@
<template>
<div
class="my-6 block w-full divide-y divide-dividerLight border border-dividerLight lg:my-0 lg:flex lg:divide-x lg:divide-y-0 lg:border-0"
>
<div class="table-box font-mono text-tiny">
{{ shortcode.id }}
</div>
<div class="table-box" :class="requestLabelColor">
{{ parseShortcodeRequest.method }}
</div>
<div class="table-box">
{{ parseShortcodeRequest.endpoint }}
</div>
<div ref="timeStampRef" class="table-box">
{{ dateStamp }}
</div>
<div class="table-box justify-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.open_workspace')"
:to="`${shortcodeBaseURL}/r/${shortcode.id}`"
blank
:icon="IconExternalLink"
class="px-3 text-accent hover:text-accent"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
color="green"
:icon="copyIconRefs"
class="px-3"
@click="copyShortcode(shortcode.id)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.delete')"
:icon="IconTrash"
color="red"
class="px-3"
@click="deleteShortcode(shortcode.id)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue"
import { pipe } from "fp-ts/function"
import * as RR from "fp-ts/ReadonlyRecord"
import * as O from "fp-ts/Option"
import { translateToNewRequest } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { Shortcode } from "~/helpers/shortcodes/Shortcode"
import { shortDateTime } from "~/helpers/utils/date"
import IconTrash from "~icons/lucide/trash"
import IconExternalLink from "~icons/lucide/external-link"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
shortcode: Shortcode
}>()
const emit = defineEmits<{
(e: "delete-shortcode", codeID: string): void
}>()
const deleteShortcode = (codeID: string) => {
emit("delete-shortcode", codeID)
}
const requestMethodLabels = {
get: "text-green-500",
post: "text-yellow-500",
put: "text-blue-500",
delete: "text-red-500",
default: "text-gray-500",
} as const
const timeStampRef = ref()
const copyIconRefs = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const parseShortcodeRequest = computed(() =>
pipe(props.shortcode.request, JSON.parse, translateToNewRequest)
)
const requestLabelColor = computed(() =>
pipe(
requestMethodLabels,
RR.lookup(parseShortcodeRequest.value.method.toLowerCase()),
O.getOrElseW(() => requestMethodLabels.default)
)
)
const dateStamp = computed(() => shortDateTime(props.shortcode.createdOn))
const shortcodeBaseURL =
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const copyShortcode = (codeID: string) => {
copyToClipboard(`${shortcodeBaseURL}/r/${codeID}`)
toast.success(`${t("state.copied_to_clipboard")}`)
copyIconRefs.value = IconCheck
}
</script>
<style lang="scss" scoped>
.table-box {
@apply flex flex-1 items-center truncate px-4 py-1;
}
</style>

View File

@@ -1,168 +0,0 @@
<template>
<section class="p-4">
<h4 class="font-semibold text-secondaryDark">
{{ t("settings.short_codes") }}
</h4>
<div class="my-1 text-secondaryLight">
{{ t("settings.short_codes_description") }}
</div>
<div class="relative overflow-x-auto py-4">
<div v-if="loading" class="flex flex-col items-center justify-center">
<HoppSmartSpinner class="mb-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<HoppSmartPlaceholder
v-if="!loading && myShortcodes.length === 0"
:src="`/images/states/${colorMode.value}/add_files.svg`"
:alt="`${t('empty.shortcodes')}`"
:text="t('empty.shortcodes')"
>
</HoppSmartPlaceholder>
<div v-else-if="!loading">
<div
class="hidden w-full rounded-t border-x border-t border-dividerLight bg-primaryLight lg:flex"
>
<div class="flex w-full overflow-y-scroll">
<div class="table-box">
{{ t("shortcodes.short_code") }}
</div>
<div class="table-box">
{{ t("shortcodes.method") }}
</div>
<div class="table-box">
{{ t("shortcodes.url") }}
</div>
<div class="table-box">
{{ t("shortcodes.created_on") }}
</div>
<div class="table-box justify-center">
{{ t("shortcodes.actions") }}
</div>
</div>
</div>
<div
class="flex max-h-sm w-full flex-col items-center justify-between divide-dividerLight overflow-y-scroll rounded border border-dividerLight lg:divide-y lg:rounded-t-none"
>
<ProfileShortcode
v-for="(shortcode, shortcodeIndex) in myShortcodes"
:key="`shortcode-${shortcodeIndex}`"
:shortcode="shortcode"
@delete-shortcode="deleteShortcode"
/>
<HoppSmartIntersection
v-if="hasMoreShortcodes && myShortcodes.length > 0"
@intersecting="loadMoreShortcodes()"
>
<div v-if="adapterLoading" class="flex flex-col items-center py-3">
<HoppSmartSpinner />
</div>
</HoppSmartIntersection>
</div>
</div>
<div
v-if="!loading && adapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="svg-icons mb-4" />
{{ getErrorMessage(adapterError) }}
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, watchEffect, computed } from "vue"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { GQLError } from "~/helpers/backend/GQLClient"
import { platform } from "~/platform"
import { onAuthEvent, onLoggedIn } from "@composables/auth"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming"
import { usePageHead } from "@composables/head"
import ShortcodeListAdapter from "~/helpers/shortcodes/ShortcodeListAdapter"
import { deleteShortcode as backendDeleteShortcode } from "~/helpers/backend/mutations/Shortcode"
const t = useI18n()
const toast = useToast()
const colorMode = useColorMode()
usePageHead({
title: computed(() => t("navigation.profile")),
})
const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const displayName = ref(currentUser.value?.displayName)
watchEffect(() => (displayName.value = currentUser.value?.displayName))
const emailAddress = ref(currentUser.value?.email)
watchEffect(() => (emailAddress.value = currentUser.value?.email))
const adapter = new ShortcodeListAdapter(true)
const adapterLoading = useReadonlyStream(adapter.loading$, false)
const adapterError = useReadonlyStream(adapter.error$, null)
const myShortcodes = useReadonlyStream(adapter.shortcodes$, [])
const hasMoreShortcodes = useReadonlyStream(adapter.hasMoreShortcodes$, true)
const loading = computed(
() => adapterLoading.value && myShortcodes.value.length === 0
)
onLoggedIn(() => {
try {
adapter.initialize()
} catch (e) {}
})
onAuthEvent((ev) => {
if (ev.event === "logout" && adapter.isInitialized()) {
adapter.dispose()
return
}
})
const deleteShortcode = (codeID: string) => {
pipe(
backendDeleteShortcode(codeID),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
},
() => {
toast.success(`${t("shortcodes.deleted")}`)
}
)
)()
}
const loadMoreShortcodes = () => {
adapter.loadMore()
}
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "shortcode/not_found":
return t("shortcodes.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script>
<style lang="scss" scoped>
.table-box {
@apply flex flex-1 items-center truncate px-4 py-2;
}
</style>

View File

@@ -174,12 +174,7 @@ const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "shortcode/not_found":
return t("shortcodes.not_found")
default:
return t("error.something_went_wrong")
}
return t("error.something_went_wrong")
}
}
</script>

View File

@@ -0,0 +1,140 @@
<template>
<div
v-if="selectedWidget"
class="divide-y divide-divider rounded border border-divider"
>
<div v-if="loading" class="px-4 py-2">
{{ t("shared_requests.creating_widget") }}
</div>
<div v-else class="px-4 py-2">
{{ t("shared_requests.description") }}
</div>
<div class="flex flex-col divide-y divide-divider">
<div class="flex flex-col space-y-4 p-4">
<div
v-for="widget in widgets"
:key="widget.value"
class="flex cursor-pointer flex-col space-y-2 rounded border border-divider px-4 py-3 hover:bg-dividerLight"
:class="{
'!border-accentLight': selectedWidget.value === widget.value,
}"
@click="selectedWidget = widget"
>
<span class="text-md font-bold">
{{ widget.label }}
</span>
<span class="text-tiny">
{{ widget.info }}
</span>
</div>
</div>
<div class="flex flex-col divide-y divide-divider">
<div class="px-4 py-3">{{ t("shared_requests.preview") }}</div>
<div class="flex flex-col items-center justify-center px-4 py-10">
<ShareTemplatesEmbeds
v-if="selectedWidget.value === 'embed'"
:endpoint="request?.endpoint"
:method="request?.method"
:model-value="embedOption"
/>
<ShareTemplatesButton
v-else-if="selectedWidget.value === 'button'"
img="badge.svg"
/>
<ShareTemplatesLink v-else />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { PropType, ref } from "vue"
import { useI18n } from "~/composables/i18n"
const t = useI18n()
const props = defineProps({
request: {
type: Object as PropType<HoppRESTRequest | null>,
required: true,
},
modelValue: {
type: Object as PropType<Widget | null>,
default: null,
},
loading: {
type: Boolean,
default: false,
},
})
const selectedWidget = useVModel(props, "modelValue")
type WidgetID = "embed" | "button" | "link"
type Widget = {
value: WidgetID
label: string
info: string
}
const widgets: Widget[] = [
{
value: "embed",
label: t("shared_requests.embed"),
info: t("shared_requests.embed_info"),
},
{
value: "button",
label: t("shared_requests.button"),
info: t("shared_requests.button_info"),
},
{
value: "link",
label: t("shared_requests.link"),
info: t("shared_requests.link_info"),
},
]
type Tabs = "parameters" | "body" | "headers" | "authorization"
type EmbedOption = {
selectedTab: Tabs
tabs: {
value: Tabs
label: string
enabled: boolean
}[]
theme: "light" | "dark" | "system"
}
const embedOption = 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",
})
</script>

View File

@@ -0,0 +1,418 @@
<template>
<div
v-if="selectedWidget"
class="divide-y divide-divider rounded border border-divider"
>
<div v-if="loading" class="px-4 py-2">
{{ t("shared_requests.creating_widget") }}
</div>
<div v-else class="px-4 py-2">
{{ t("shared_requests.customize") }}
</div>
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-else class="flex flex-col divide-y divide-divider">
<div class="flex flex-col space-y-4 p-4">
<HoppSmartRadioGroup
v-model="selectedWidget.value"
:radios="widgets"
class="flex !flex-row"
/>
</div>
<div class="flex flex-col divide-y divide-divider">
<div class="flex items-center justify-center px-4 py-8">
<div v-if="selectedWidget.value === 'embed'" class="w-full flex-1">
<div class="flex flex-col pb-8">
<div
v-for="option in embedOptions.tabs"
:key="option.value"
class="flex justify-between py-2"
>
<span class="capitalize">
{{ option.label }}
</span>
<HoppSmartCheckbox
:on="option.enabled"
@change="removeEmbedOption(option.value)"
/>
</div>
<div class="flex items-center justify-between">
<span>
{{ t("shared_requests.theme.title") }}
</span>
<div>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<span class="select-wrapper">
<HoppButtonSecondary
class="ml-2 rounded-none pr-8 capitalize"
:label="embedOptions.theme"
:icon="embedThemeIcon"
/>
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:label="t('shared_requests.theme.system')"
:icon="IconMonitor"
:active="embedOptions.theme === 'system'"
@click="
() => {
embedOptions.theme = 'system'
hide()
}
"
/>
<HoppSmartItem
:label="t('shared_requests.theme.light')"
:icon="IconSun"
:active="embedOptions.theme === 'light'"
@click="
() => {
embedOptions.theme = 'light'
hide()
}
"
/>
<HoppSmartItem
:label="t('shared_requests.theme.dark')"
:icon="IconMoon"
:active="embedOptions.theme === 'dark'"
@click="
() => {
embedOptions.theme = 'dark'
hide()
}
"
/>
</div>
</template>
</tippy>
</div>
</div>
</div>
<ShareTemplatesEmbeds
:endpoint="request?.endpoint"
:method="request?.method"
:model-value="embedOptions"
/>
<div class="flex items-center justify-center py-4">
<HoppButtonSecondary
:label="t('shared_requests.copy_html')"
@click="
copyContent({
widget: 'embed',
type: 'html',
})
"
/>
</div>
</div>
<div
v-else-if="selectedWidget.value === 'button'"
class="flex flex-col space-y-8"
>
<div
v-for="variant in buttonVariants"
:key="variant.id"
class="flex flex-col space-y-4"
>
<ShareTemplatesButton :img="variant.img" />
<div class="flex items-center justify-between">
<HoppButtonSecondary
:label="t('shared_requests.copy_html')"
@click="
copyContent({
widget: 'button',
type: 'html',
id: variant.id,
})
"
/>
<HoppButtonSecondary
:label="t('shared_requests.copy_markdown')"
@click="
copyContent({
widget: 'button',
type: 'markdown',
id: variant.id,
})
"
/>
</div>
</div>
</div>
<div v-else class="flex flex-col space-y-8">
<div
v-for="variant in linkVariants"
:key="variant.type"
class="flex flex-col items-center justify-center space-y-2"
>
<ShareTemplatesLink :link="variant.link" :label="variant.label" />
<HoppButtonSecondary
:label="t(`shared_requests.copy_${variant.type}`)"
@click="
copyContent({
widget: 'link',
type: variant.type,
id: variant.id,
})
"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useVModel } from "@vueuse/core"
import { computed, ref } from "vue"
import { PropType } from "vue"
import { useI18n } from "~/composables/i18n"
import IconMonitor from "~icons/lucide/monitor"
import IconSun from "~icons/lucide/sun"
import IconMoon from "~icons/lucide/moon"
import { TippyComponent } from "vue-tippy"
import { HoppRESTRequest } from "@hoppscotch/data"
const t = useI18n()
const props = defineProps({
request: {
type: Object as PropType<HoppRESTRequest | null>,
required: true,
},
modelValue: {
type: Object as PropType<Widget | null>,
default: null,
},
loading: {
type: Boolean,
default: false,
},
})
const emit = defineEmits<{
(
e: "copy-shared-request",
request: {
sharedRequestID: string | undefined
content: string | undefined
}
): void
(e: "hide-modal"): void
(e: "update:modelValue", value: string): void
}>()
const selectedWidget = useVModel(props, "modelValue")
type WidgetID = "embed" | "button" | "link"
type Widget = {
value: WidgetID
label: string
}
const widgets: Widget[] = [
{
value: "embed",
label: t("shared_requests.embed"),
},
{
value: "button",
label: t("shared_requests.button"),
},
{
value: "link",
label: t("shared_requests.link"),
},
]
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
type EmbedOption = {
selectedTab: EmbedTabs
tabs: {
value: EmbedTabs
label: string
enabled: boolean
}[]
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(() => {
if (embedOptions.value.theme === "system") {
return IconMonitor
} else if (embedOptions.value.theme === "light") {
return IconSun
} else {
return IconMoon
}
})
const removeEmbedOption = (option: EmbedTabs) => {
const index = embedOptions.value.tabs.findIndex((tab) => tab.value === option)
if (index === -1) return
//if removed tab is the selected tab, select the next tab with enabled true
if (embedOptions.value.selectedTab === option) {
const nextTab = embedOptions.value.tabs.find((tab) => tab.enabled)
if (nextTab) {
embedOptions.value.selectedTab = nextTab.value
}
}
embedOptions.value.tabs[index].enabled =
!embedOptions.value.tabs[index].enabled
}
type ButtonVariant = {
id: string
img: string
}
const buttonVariants: ButtonVariant[] = [
{
id: "button1",
img: "badge.svg",
},
{
id: "button2",
img: "badge-light.svg",
},
{
id: "button3",
img: "badge-dark.svg",
},
]
type LinkVariant = {
id: string
link?: string
label?: string
type: "html" | "markdown" | "link"
}
const linkVariants: LinkVariant[] = [
{
id: "link1",
link: props.request?.id,
type: "link",
},
{
id: "link2",
label: "shared_requests.run_in_hoppscotch",
type: "html",
},
{
id: "link3",
label: "shared_requests.run_in_hoppscotch",
type: "markdown",
},
]
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const copyEmbed = () => {
const options = embedOptions.value
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 = (
variationID: string,
type: "html" | "markdown" | "link"
) => {
let badge = ""
if (variationID === "button1") {
badge = "badge.svg"
} else if (variationID === "button2") {
badge = "badge-light.svg"
} else {
badge = "badge-dark.svg"
}
if (type === "markdown") {
return `[![Run in Hoppscotch](${baseURL}/${badge})](${baseURL}/r/${props.request?.id})`
} else {
return `<a href="${baseURL}/r/${props.request?.id}"><img src="${baseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
}
}
const copyLink = (variationID: string) => {
if (variationID === "link1") {
return `${baseURL}/r/${props.request?.id}`
} else if (variationID === "link2") {
return `<a href="${baseURL}/r/${props.request?.id}">Run in Hoppscotch</a>`
} else {
return `[Run in Hoppscotch](${baseURL}/r/${props.request?.id})`
}
}
const copyContent = ({
id,
widget,
type,
}: {
id?: string | undefined
widget: WidgetID
type: "html" | "markdown" | "link"
}) => {
let content = ""
if (widget === "button") {
content = copyButton(id!, type)
} else if (widget === "link") {
content = copyLink(id!)
} else {
content = copyEmbed()
}
const copyContent = { sharedRequestID: props.request?.id, content }
emit("copy-shared-request", copyContent)
}
const tippyActions = ref<TippyComponent | null>(null)
</script>

View File

@@ -0,0 +1,123 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('modal.share_request')"
styles="sm:max-w-md"
@close="hideModal"
>
<template #body>
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<ShareCreateModal
v-else-if="step === 1"
v-model="selectedWidget"
:request="request"
:loading="loading"
@create-shared-request="createSharedRequest"
/>
<ShareCustomizeModal
v-else-if="step === 2"
v-model="selectedWidget"
:request="request"
:loading="loading"
@copy-shared-request="copySharedRequest"
/>
</template>
<template #footer>
<div v-if="step === 1" class="flex justify-end">
<HoppButtonPrimary
:label="t('action.create')"
:loading="loading"
@click="createSharedRequest"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
class="mr-2"
@click="hideModal"
/>
</div>
<HoppButtonPrimary v-else :label="t('action.close')" @click="hideModal" />
</template>
</HoppSmartModal>
</template>
<script lang="ts" setup>
import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { PropType } from "vue"
import { useI18n } from "~/composables/i18n"
const t = useI18n()
const props = defineProps({
request: {
type: Object as PropType<HoppRESTRequest | null>,
required: true,
},
show: {
type: Boolean,
default: false,
required: true,
},
modelValue: {
type: Object as PropType<Widget | null>,
default: null,
},
loading: {
type: Boolean,
default: false,
},
step: {
type: Number,
default: 1,
},
})
type WidgetID = "embed" | "button" | "link"
type Widget = {
value: WidgetID
label: string
info: string
}
const selectedWidget = useVModel(props, "modelValue")
const emit = defineEmits<{
(e: "create-shared-request", request: HoppRESTRequest | null): void
(e: "hide-modal"): void
(e: "update:modelValue", value: string): void
(e: "update:step", value: number): void
(
e: "copy-shared-request",
request: {
sharedRequestID: string | undefined
content: string | undefined
}
): void
}>()
const createSharedRequest = () => {
emit("create-shared-request", props.request as HoppRESTRequest)
}
const copySharedRequest = (request: {
sharedRequestID: string | undefined
content: string | undefined
}) => {
emit("copy-shared-request", request)
}
const hideModal = () => {
emit("hide-modal")
selectedWidget.value = {
value: "embed",
label: t("shared_requests.embed"),
info: t("shared_requests.embed_info"),
}
}
</script>

View File

@@ -0,0 +1,155 @@
<template>
<div
class="group flex items-stretch"
@contextmenu.prevent="options!.tippy.show()"
>
<div
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"
:title="`${timeStamp}`"
@click="openInNewTab"
>
<span
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
:style="{ color: requestLabelColor }"
>
<span class="truncate text-tiny font-semibold">
{{ parseRequest.method }}
</span>
</span>
<span
class="pointer-events-none flex min-w-0 flex-1 items-center pr-2 transition group-hover:text-secondaryDark"
>
<span class="flex-1 truncate">
{{ parseRequest.endpoint }}
</span>
</span>
<span
class="flex-1 truncate border-l border-dividerDark px-2 text-secondaryLight group-hover:text-secondaryDark"
>
{{ parseRequest.name }}
</span>
</div>
<div>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
role="menu"
@keyup.t="openInNewTabAction?.$el.click()"
@keyup.e="customizeAction?.$el.click()"
@keyup.delete="deleteAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
ref="openInNewTabAction"
:icon="IconArrowUpRight"
:label="`${t('shared_requests.open_new_tab')}`"
:shortcut="['T']"
@click="
() => {
openInNewTab()
hide()
}
"
/>
<HoppSmartItem
ref="customizeAction"
:icon="IconFileEdit"
:label="`${t('shared_requests.customize')}`"
:shortcut="['E']"
@click="
() => {
customizeSharedRequest()
hide()
}
"
/>
<HoppSmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="`${t('action.delete')}`"
:shortcut="['⌫']"
@click="
() => {
deleteSharedRequest()
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
import { pipe } from "fp-ts/lib/function"
import { ref } from "vue"
import { computed } from "vue"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import { Shortcode } from "~/helpers/shortcode/Shortcode"
import IconArrowUpRight from "~icons/lucide/arrow-up-right-square"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconFileEdit from "~icons/lucide/file-edit"
import IconTrash2 from "~icons/lucide/trash-2"
import { shortDateTime } from "~/helpers/utils/date"
const t = useI18n()
const props = defineProps<{
request: Shortcode
}>()
const emit = defineEmits<{
(e: "customize-shared-request", request: HoppRESTRequest, id: string): void
(e: "delete-shared-request", codeID: string): void
(e: "open-new-tab", request: HoppRESTRequest): void
}>()
const tippyActions = ref<TippyComponent | null>(null)
const openInNewTabAction = ref<HTMLButtonElement | null>(null)
const customizeAction = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null)
const parseRequest = computed(() =>
pipe(props.request.request, JSON.parse, translateToNewRequest)
)
const requestLabelColor = computed(() =>
getMethodLabelColorClassOf(parseRequest.value)
)
const openInNewTab = () => {
emit("open-new-tab", parseRequest.value)
}
const customizeSharedRequest = () => {
emit("customize-shared-request", parseRequest.value, props.request.id)
}
const deleteSharedRequest = () => {
emit("delete-shared-request", props.request.id)
}
const timeStamp = computed(() => shortDateTime(props.request.createdOn))
</script>

View File

@@ -0,0 +1,308 @@
<template>
<div>
<div
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary"
>
<WorkspaceCurrent
:section="t('tab.shared_requests')"
:is-only-personal="true"
/>
</div>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-end overflow-x-auto border-b border-dividerLight bg-primary"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/shared-request"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
class="py-2"
/>
</div>
<div class="flex flex-col">
<div v-if="loading" class="flex flex-col items-center justify-center">
<HoppSmartSpinner class="mb-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<HoppSmartPlaceholder
v-else-if="!currentUser"
:src="`/images/states/${colorMode.value}/add_files.svg`"
:alt="`${t('empty.shared_requests_logout')}`"
:text="`${t('empty.shared_requests_logout')}`"
>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
</HoppSmartPlaceholder>
<template v-else-if="sharedRequests.length">
<ShareRequest
v-for="request in sharedRequests"
:key="request.id"
:request="request"
@customize-shared-request="customizeSharedRequest"
@delete-shared-request="deleteSharedRequest"
@open-new-tab="openInNewTab"
/>
<HoppSmartIntersection
v-if="hasMoreSharedRequests"
@intersecting="loadMoreSharedRequests"
>
<div v-if="adapterLoading" class="flex flex-col items-center py-3">
<HoppSmartSpinner />
</div>
</HoppSmartIntersection>
</template>
<div v-else-if="adapterError" class="flex flex-col items-center py-4">
<icon-lucide-help-circle class="svg-icons mb-4" />
{{ getErrorMessage(adapterError) }}
</div>
<HoppSmartPlaceholder
v-else
:src="`/images/states/${colorMode.value}/add_files.svg`"
:alt="`${t('empty.shared_requests')}`"
:text="t('empty.shared_requests')"
@drop.stop
/>
</div>
</div>
<HoppSmartConfirmModal
:show="showConfirmModal"
:title="confirmModalTitle"
:loading-state="modalLoadingState"
@hide-modal="showConfirmModal = false"
@resolve="resolveConfirmModal"
/>
<ShareModal
v-model="selectedWidget"
:request="requestToShare"
:show="showShareRequestModal"
:loading="shareRequestCreatingLoading"
:step="step"
@hide-modal="displayCustomizeRequestModal(false)"
@copy-shared-request="copySharedRequest"
@create-shared-request="createSharedRequest"
/>
</template>
<script lang="ts" setup>
import IconHelpCircle from "~icons/lucide/help-circle"
import { useI18n } from "~/composables/i18n"
import ShortcodeListAdapter from "~/helpers/shortcode/ShortcodeListAdapter"
import { useReadonlyStream } from "~/composables/stream"
import { onAuthEvent, onLoggedIn } from "~/composables/auth"
import { computed } from "vue"
import { useColorMode } from "~/composables/theming"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { platform } from "~/platform"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import {
deleteShortcode as backendDeleteShortcode,
createShortcode,
} from "~/helpers/backend/mutations/Shortcode"
import { GQLError } from "~/helpers/backend/GQLClient"
import { useToast } from "~/composables/toast"
import { ref } from "vue"
import { HoppRESTRequest } from "@hoppscotch/data"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import * as E from "fp-ts/Either"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n()
const colorMode = useColorMode()
const toast = useToast()
const showConfirmModal = ref(false)
const confirmModalTitle = ref("")
const modalLoadingState = ref(false)
const showShareRequestModal = ref(false)
const sharedRequestID = ref("")
const shareRequestCreatingLoading = ref(false)
const requestToShare = ref<HoppRESTRequest | null>(null)
const restTab = useService(RESTTabService)
const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const step = ref(1)
type WidgetID = "embed" | "button" | "link"
type Widget = {
value: WidgetID
label: string
info: string
}
const selectedWidget = ref<Widget>({
value: "embed",
label: t("shared_requests.embed"),
info: t("shared_requests.embed_info"),
})
const adapter = new ShortcodeListAdapter(true)
const adapterLoading = useReadonlyStream(adapter.loading$, false)
const adapterError = useReadonlyStream(adapter.error$, null)
const sharedRequests = useReadonlyStream(adapter.shortcodes$, [])
const hasMoreSharedRequests = useReadonlyStream(
adapter.hasMoreShortcodes$,
true
)
const loading = computed(
() => adapterLoading.value && sharedRequests.value.length === 0
)
onLoggedIn(() => {
try {
adapter.initialize()
} catch (e) {
console.error(e)
}
})
onAuthEvent((ev) => {
if (ev.event === "logout" && adapter.isInitialized()) {
adapter.dispose()
return
}
})
const deleteSharedRequest = (codeID: string) => {
if (currentUser.value) {
sharedRequestID.value = codeID
confirmModalTitle.value = `${t("confirm.remove_shared_request")}`
showConfirmModal.value = true
} else {
invokeAction("modals.login.toggle")
}
}
const onDeleteSharedRequest = () => {
modalLoadingState.value = true
pipe(
backendDeleteShortcode(sharedRequestID.value),
TE.match(
(err: GQLError<string>) => {
toast.error(getErrorMessage(err))
showConfirmModal.value = false
},
() => {
toast.success(t("shared_requests.deleted"))
sharedRequestID.value = ""
modalLoadingState.value = false
showConfirmModal.value = false
}
)
)()
}
const loadMoreSharedRequests = () => {
adapter.loadMore()
}
const displayShareRequestModal = (show: boolean) => {
showShareRequestModal.value = show
step.value = 1
}
const displayCustomizeRequestModal = (show: boolean) => {
showShareRequestModal.value = show
step.value = 2
}
const createSharedRequest = async (request: HoppRESTRequest | null) => {
if (request && selectedWidget.value) {
shareRequestCreatingLoading.value = true
const sharedRequestResult = await createShortcode(request)()
platform.analytics?.logEvent({
type: "HOPP_SHORTCODE_CREATED",
})
if (E.isLeft(sharedRequestResult)) {
toast.error(`${sharedRequestResult.left.error}`)
toast.error(t("error.something_went_wrong"))
} else if (E.isRight(sharedRequestResult)) {
if (sharedRequestResult.right.createShortcode) {
shareRequestCreatingLoading.value = false
requestToShare.value = {
...JSON.parse(sharedRequestResult.right.createShortcode.request),
id: sharedRequestResult.right.createShortcode.id,
}
step.value = 2
}
}
}
}
const customizeSharedRequest = (
request: HoppRESTRequest,
shredRequestID: string
) => {
requestToShare.value = {
...request,
id: shredRequestID,
}
displayCustomizeRequestModal(true)
}
const copySharedRequest = (request: {
sharedRequestID: string | undefined
content: string | undefined
}) => {
if (request.content) {
copyToClipboard(request.content)
toast.success(t("state.copied_to_clipboard"))
}
}
const openInNewTab = (request: HoppRESTRequest) => {
restTab.createNewTab({
isDirty: false,
request,
})
}
const resolveConfirmModal = (title: string | null) => {
if (title === `${t("confirm.remove_shared_request")}`) onDeleteSharedRequest()
else {
console.error(
`Confirm modal title ${title} is not handled by the component`
)
toast.error(t("error.something_went_wrong"))
showConfirmModal.value = false
sharedRequestID.value = ""
}
}
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "shortcode/not_found":
return t("shared_request.not_found")
default:
return t("error.something_went_wrong")
}
}
}
defineActionHandler("share.request", ({ request }) => {
requestToShare.value = request
displayShareRequestModal(true)
})
</script>

View File

@@ -0,0 +1,19 @@
<template>
<div
class="flex items-center justify-center rounded border border-dotted border-dividerDark p-5"
>
<a href="/" target="_blank">
<img :src="img" :alt="t('shared_requests.run_in_hoppscotch')" />
</a>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "~/composables/i18n"
defineProps<{
img: string
}>()
const t = useI18n()
</script>

View File

@@ -0,0 +1,101 @@
<template>
<div
class="flex flex-col rounded border border-dotted border-divider p-5"
:class="{
'bg-accentContrast': isEmbedThemeLight,
}"
>
<div
class="flex items-stretch space-x-4 rounded border-divider"
:class="{
'bg-accentContrast': isEmbedThemeLight,
}"
>
<span
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
class="min-w-0 truncate"
:class="{
'text-primary': isEmbedThemeLight,
}"
>
{{ endpoint }}
</span>
</span>
<button
class="flex items-center justify-center rounded border border-dividerDark bg-primaryDark px-3 py-2 font-semibold text-secondary"
:class="{
'!bg-accentContrast text-primaryLight': isEmbedThemeLight,
}"
>
{{ t("action.send") }}
</button>
</div>
<div
class="flex border-divider"
:class="{
'bg-accentContrast text-primary': isEmbedThemeLight,
'border-b pt-2 ': !noActiveTab,
}"
>
<span
v-for="option in embedOptions.tabs"
v-show="option.enabled"
:key="option.value"
class="px-2 py-2"
:class="{
'border-b border-dividerDark':
embedOptions.selectedTab === option.value,
}"
>
{{ option.label }}
</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { useVModel } from "@vueuse/core"
import { computed } from "vue"
import { useI18n } from "~/composables/i18n"
type Tabs = "parameters" | "body" | "headers" | "authorization"
type EmbedOption = {
selectedTab: Tabs
tabs: {
value: Tabs
label: string
enabled: boolean
}[]
theme: "light" | "dark" | "system"
}
const props = defineProps<{
method: string | undefined
endpoint: string | undefined
modelValue: EmbedOption
}>()
const embedOptions = useVModel(props, "modelValue")
const t = useI18n()
const noActiveTab = computed(() => {
return embedOptions.value.tabs.every((tab) => !tab.enabled)
})
const isEmbedThemeLight = computed(() => embedOptions.value.theme === "light")
</script>

View File

@@ -0,0 +1,29 @@
<template>
<div
class="flex items-center justify-center rounded border border-dotted border-dividerDark p-5"
>
<span
:class="{
'border-b border-secondary': label,
}"
>
{{ text }}
</span>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue"
import { useI18n } from "~/composables/i18n"
const t = useI18n()
const props = defineProps<{
link?: string | undefined
label?: string | undefined
}>()
const text = computed(() => {
return props.label ? t(props.label) : `hopp.sh/r/${props.link ?? "xxxx"}`
})
</script>

View File

@@ -3,11 +3,7 @@
class="flex items-center overflow-x-auto whitespace-nowrap border-b border-dividerLight px-4 py-2 text-tiny text-secondaryLight"
>
<span class="truncate">
{{
workspace.type === "personal"
? t("workspace.personal")
: teamWorkspaceName
}}
{{ currentWorkspace }}
</span>
<icon-lucide-chevron-right v-if="section" class="mx-2" />
{{ section }}
@@ -20,8 +16,9 @@ import { useI18n } from "~/composables/i18n"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
defineProps<{
const props = defineProps<{
section?: string
isOnlyPersonal?: boolean
}>()
const t = useI18n()
@@ -29,11 +26,20 @@ const t = useI18n()
const workspaceService = useService(WorkspaceService)
const workspace = workspaceService.currentWorkspace
const currentWorkspace = computed(() => {
if (props.isOnlyPersonal) {
return `${t("workspace.personal")}`
}
if (workspace.value.type === "team") {
return teamWorkspaceName.value
}
return `${t("workspace.personal")}`
})
const teamWorkspaceName = computed(() => {
if (workspace.value.type === "team" && workspace.value.teamName) {
return workspace.value.teamName
} else {
return `${t("workspace.team")}`
}
return `${t("workspace.team")}`
})
</script>