Files
hoppscotch/packages/hoppscotch-common/src/components/share/index.vue
2023-12-12 14:26:13 +05:30

465 lines
12 KiB
Vue

<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')}`"
>
<template #body>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
</template>
</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"
v-model:embed-options="embedOptions"
:step="step"
:request="requestToShare"
:show="showShareRequestModal"
:loading="shareRequestCreatingLoading"
@hide-modal="displayCustomizeRequestModal(false, null)"
@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,
updateEmbedProperties,
} 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"
import { watch } from "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 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 currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
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 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,
embedProperties?: string | null
) => {
showShareRequestModal.value = show
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) => {
if (request && selectedWidget.value) {
const properties = {
options: ["parameters", "body", "headers"],
theme: "system",
}
shareRequestCreatingLoading.value = true
const sharedRequestResult = await createShortcode(
request,
selectedWidget.value.value === "embed"
? JSON.stringify(properties)
: undefined
)()
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
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,
}
}
}
}
}
}
const customizeSharedRequest = (
request: HoppRESTRequest,
shredRequestID: string,
embedProperties?: string | null
) => {
requestToShare.value = {
...request,
id: shredRequestID,
}
displayCustomizeRequestModal(true, embedProperties)
}
const copySharedRequest = (payload: {
sharedRequestID: string | undefined
content: string | undefined
}) => {
if (payload.content) {
copyToClipboard(payload.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")
}
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>