feat: shared request (#3486)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"connect": "Connect",
|
||||
"connecting": "Connecting",
|
||||
"copy": "Copy",
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"disconnect": "Disconnect",
|
||||
"dismiss": "Dismiss",
|
||||
@@ -40,6 +41,7 @@
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"search": "Search",
|
||||
"send": "Send",
|
||||
"share": "Share",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
@@ -188,6 +190,7 @@
|
||||
"remove_folder": "Are you sure you want to permanently delete this folder?",
|
||||
"remove_history": "Are you sure you want to permanently delete all history?",
|
||||
"remove_request": "Are you sure you want to permanently delete this request?",
|
||||
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||
"remove_team": "Are you sure you want to delete this team?",
|
||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
@@ -229,7 +232,8 @@
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protocols are empty",
|
||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Team name empty",
|
||||
"teams": "You don't belong to any teams",
|
||||
@@ -416,6 +420,7 @@
|
||||
"collections": "Collections",
|
||||
"confirm": "Confirm",
|
||||
"edit_request": "Edit Request",
|
||||
"share_request":"Share Request",
|
||||
"import_export": "Import / Export"
|
||||
},
|
||||
"mqtt": {
|
||||
@@ -491,7 +496,6 @@
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
},
|
||||
"copy_link": "Copy link",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"duration": "Duration",
|
||||
@@ -524,6 +528,7 @@
|
||||
"saved": "Request saved",
|
||||
"share": "Share",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"share_request": "Share Request",
|
||||
"stop": "Stop",
|
||||
"title": "Request",
|
||||
"type": "Request type",
|
||||
@@ -604,14 +609,30 @@
|
||||
"additional": "Additional Settings",
|
||||
"verify_email": "Verify email"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
"shared_requests":{
|
||||
"button":"Button",
|
||||
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||
"customize": "Customize",
|
||||
"creating_widget": "Creating widget",
|
||||
"copy_html": "Copy HTML",
|
||||
"copy_link": "Copy Link",
|
||||
"copy_markdown": "Copy Markdown",
|
||||
"deleted":"Shared request deleted",
|
||||
"description": "Select a widget, you can change and customize this later",
|
||||
"embed":"Embed",
|
||||
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||
"link":"Link",
|
||||
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||
"not_found":"Shared request not found",
|
||||
"open_new_tab": "Open in new tab",
|
||||
"preview":"Preview",
|
||||
"run_in_hoppscotch":"Run in Hoppscotch",
|
||||
"theme":{
|
||||
"dark":"Dark",
|
||||
"light":"Light",
|
||||
"system" :"System",
|
||||
"title":"Theme"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"general": {
|
||||
@@ -641,7 +662,6 @@
|
||||
"title": "Others"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "Copy Request Link",
|
||||
"delete_method": "Select DELETE method",
|
||||
"get_method": "Select GET method",
|
||||
"head_method": "Select HEAD method",
|
||||
@@ -657,6 +677,7 @@
|
||||
"save_to_collections": "Save to Collections",
|
||||
"send_request": "Send Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"share_request":"Share Request",
|
||||
"title": "Request"
|
||||
},
|
||||
"response": {
|
||||
@@ -840,6 +861,7 @@
|
||||
"queries": "Queries",
|
||||
"query": "Query",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Tests",
|
||||
|
||||
10
packages/hoppscotch-common/src/components.d.ts
vendored
10
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -185,6 +185,16 @@ declare module 'vue' {
|
||||
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
||||
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
||||
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
||||
Share: typeof import('./components/share/index.vue')['default']
|
||||
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
|
||||
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
|
||||
ShareModal: typeof import('./components/share/Modal.vue')['default']
|
||||
ShareRequest: typeof import('./components/share/Request.vue')['default']
|
||||
ShareRequestModal: typeof import('./components/share/RequestModal.vue')['default']
|
||||
ShareShareRequestModal: typeof import('./components/share/ShareRequestModal.vue')['default']
|
||||
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
|
||||
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
|
||||
ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
|
||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
140
packages/hoppscotch-common/src/components/share/CreateModal.vue
Normal file
140
packages/hoppscotch-common/src/components/share/CreateModal.vue
Normal 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>
|
||||
@@ -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 `[](${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>
|
||||
123
packages/hoppscotch-common/src/components/share/Modal.vue
Normal file
123
packages/hoppscotch-common/src/components/share/Modal.vue
Normal 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>
|
||||
155
packages/hoppscotch-common/src/components/share/Request.vue
Normal file
155
packages/hoppscotch-common/src/components/share/Request.vue
Normal 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>
|
||||
308
packages/hoppscotch-common/src/components/share/index.vue
Normal file
308
packages/hoppscotch-common/src/components/share/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -15,7 +15,7 @@ export type HoppAction =
|
||||
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
||||
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
|
||||
| "request.reset" // Clear request data
|
||||
| "request.copy-link" // Copy Request Link
|
||||
| "request.share-request" // Share Request
|
||||
| "request.save" // Save to Collections
|
||||
| "request.save-as" // Save As
|
||||
| "request.rename" // Rename request on REST or GraphQL
|
||||
@@ -115,7 +115,9 @@ type HoppActionArgsMap = {
|
||||
"request.open-tab": {
|
||||
tab: RESTOptionTabs | GQLOptionTabs
|
||||
}
|
||||
|
||||
"share.request": {
|
||||
request: HoppRESTRequest
|
||||
}
|
||||
"tab.duplicate-tab": {
|
||||
tabID?: string
|
||||
}
|
||||
|
||||
@@ -2,5 +2,7 @@ mutation CreateShortcode($request: String!) {
|
||||
createShortcode(request: $request) {
|
||||
id
|
||||
request
|
||||
createdOn
|
||||
properties
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,6 @@ query GetUserShortcodes($cursor: ID) {
|
||||
id
|
||||
request
|
||||
createdOn
|
||||
properties
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ subscription ShortcodeCreated {
|
||||
id
|
||||
request
|
||||
createdOn
|
||||
properties
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const bindings: {
|
||||
} = {
|
||||
"ctrl-enter": "request.send-cancel",
|
||||
"ctrl-i": "request.reset",
|
||||
"ctrl-u": "request.copy-link",
|
||||
"ctrl-u": "request.share-request",
|
||||
"ctrl-s": "request.save",
|
||||
"ctrl-shift-s": "request.save-as",
|
||||
"alt-up": "request.method.next",
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
export interface Shortcode {
|
||||
id: string
|
||||
request: string
|
||||
properties?: string | null | undefined
|
||||
createdOn: Date
|
||||
}
|
||||
@@ -23,11 +23,11 @@ export default class ShortcodeListAdapter {
|
||||
|
||||
private isDispose: boolean
|
||||
|
||||
private myShortcodesCreated: Subscription | null
|
||||
private myShortcodesRevoked: Subscription | null
|
||||
private shortcodeCreated: Subscription | null
|
||||
private shortcodeRevoked: Subscription | null
|
||||
|
||||
private myShortcodesCreatedSub: WSubscription | null
|
||||
private myShortcodesRevokedSub: WSubscription | null
|
||||
private shortcodeCreatedSub: WSubscription | null
|
||||
private shortcodeRevokedSub: WSubscription | null
|
||||
|
||||
constructor(deferInit = false) {
|
||||
this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
|
||||
@@ -37,24 +37,25 @@ export default class ShortcodeListAdapter {
|
||||
>([])
|
||||
this.hasMoreShortcodes$ = new BehaviorSubject<boolean>(true)
|
||||
this.isDispose = true
|
||||
this.myShortcodesCreated = null
|
||||
this.myShortcodesRevoked = null
|
||||
this.myShortcodesCreatedSub = null
|
||||
this.myShortcodesRevokedSub = null
|
||||
this.shortcodeCreated = null
|
||||
this.shortcodeRevoked = null
|
||||
this.shortcodeCreatedSub = null
|
||||
this.shortcodeRevokedSub = null
|
||||
|
||||
if (!deferInit) this.initialize()
|
||||
}
|
||||
|
||||
unsubscribeSubscriptions() {
|
||||
this.myShortcodesCreated?.unsubscribe()
|
||||
this.myShortcodesRevoked?.unsubscribe()
|
||||
this.myShortcodesCreatedSub?.unsubscribe()
|
||||
this.myShortcodesRevokedSub?.unsubscribe()
|
||||
this.shortcodeCreated?.unsubscribe()
|
||||
this.shortcodeRevoked?.unsubscribe()
|
||||
this.shortcodeCreatedSub?.unsubscribe()
|
||||
this.shortcodeRevokedSub?.unsubscribe()
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (!this.isDispose) throw new Error(`Adapter is already initialized`)
|
||||
|
||||
this.isDispose = false
|
||||
this.fetchList()
|
||||
this.registerSubscriptions()
|
||||
}
|
||||
@@ -68,8 +69,8 @@ export default class ShortcodeListAdapter {
|
||||
|
||||
public dispose() {
|
||||
if (this.isDispose) throw new Error(`Adapter has been disposed`)
|
||||
|
||||
this.isDispose = true
|
||||
this.shortcodes$.next([])
|
||||
this.unsubscribeSubscriptions()
|
||||
}
|
||||
|
||||
@@ -98,12 +99,12 @@ export default class ShortcodeListAdapter {
|
||||
console.error(result.left)
|
||||
this.loading$.next(false)
|
||||
|
||||
throw new Error(`Failed fetching short codes list: ${result.left}`)
|
||||
throw new Error(`Failed fetching shortcodes list: ${result.left}`)
|
||||
}
|
||||
|
||||
const fetchedResult = result.right.myShortcodes
|
||||
|
||||
this.pushNewShortcodes(fetchedResult)
|
||||
this.pushNewShortcode(fetchedResult)
|
||||
|
||||
if (fetchedResult.length !== BACKEND_PAGE_SIZE) {
|
||||
this.hasMoreShortcodes$.next(false)
|
||||
@@ -112,7 +113,7 @@ export default class ShortcodeListAdapter {
|
||||
this.loading$.next(false)
|
||||
}
|
||||
|
||||
private pushNewShortcodes(results: Shortcode[]) {
|
||||
private pushNewShortcode(results: Shortcode[]) {
|
||||
const userShortcodes = this.shortcodes$.value
|
||||
|
||||
userShortcodes.push(...results)
|
||||
@@ -121,29 +122,30 @@ export default class ShortcodeListAdapter {
|
||||
}
|
||||
|
||||
private createShortcode(shortcode: Shortcode) {
|
||||
const userShortcodes = this.shortcodes$.value
|
||||
const userShortcode = this.shortcodes$.value
|
||||
|
||||
userShortcodes.unshift(shortcode)
|
||||
userShortcode.unshift(shortcode)
|
||||
|
||||
this.shortcodes$.next(userShortcodes)
|
||||
this.shortcodes$.next(userShortcode)
|
||||
}
|
||||
|
||||
private deleteShortcode(codeId: string) {
|
||||
const newShortcodes = this.shortcodes$.value.filter(
|
||||
private deleteSharedRequest(codeId: string) {
|
||||
const newShortcode = this.shortcodes$.value.filter(
|
||||
({ id }) => id !== codeId
|
||||
)
|
||||
|
||||
this.shortcodes$.next(newShortcodes)
|
||||
this.shortcodes$.next(newShortcode)
|
||||
}
|
||||
|
||||
private registerSubscriptions() {
|
||||
const [myShortcodeCreated$, myShortcodeCreatedSub] =
|
||||
runAuthOnlyGQLSubscription({
|
||||
const [shortcodeCreated$, shortcodeCreatedSub] = runAuthOnlyGQLSubscription(
|
||||
{
|
||||
query: ShortcodeCreatedDocument,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
this.myShortcodesCreatedSub = myShortcodeCreatedSub
|
||||
this.myShortcodesCreated = myShortcodeCreated$.subscribe((result) => {
|
||||
this.shortcodeCreatedSub = shortcodeCreatedSub
|
||||
this.shortcodeCreated = shortcodeCreated$.subscribe((result) => {
|
||||
if (E.isLeft(result)) {
|
||||
console.error(result.left)
|
||||
throw new Error(`Shortcode Create Error ${result.left}`)
|
||||
@@ -152,19 +154,20 @@ export default class ShortcodeListAdapter {
|
||||
this.createShortcode(result.right.myShortcodesCreated)
|
||||
})
|
||||
|
||||
const [myShortcodesRevoked$, myShortcodeRevokedSub] =
|
||||
runAuthOnlyGQLSubscription({
|
||||
const [shortcodeRevoked$, shortcodeRevokedSub] = runAuthOnlyGQLSubscription(
|
||||
{
|
||||
query: ShortcodeDeletedDocument,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
this.myShortcodesRevokedSub = myShortcodeRevokedSub
|
||||
this.myShortcodesRevoked = myShortcodesRevoked$.subscribe((result) => {
|
||||
this.shortcodeRevokedSub = shortcodeRevokedSub
|
||||
this.shortcodeRevoked = shortcodeRevoked$.subscribe((result) => {
|
||||
if (E.isLeft(result)) {
|
||||
console.error(result.left)
|
||||
throw new Error(`Shortcode Delete Error ${result.left}`)
|
||||
}
|
||||
|
||||
this.deleteShortcode(result.right.myShortcodesRevoked.id)
|
||||
this.deleteSharedRequest(result.right.myShortcodesRevoked.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export function getShortcuts(t: (x: string) => string): ShortcutDef[] {
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "U"],
|
||||
label: t("shortcut.request.copy_request_link"),
|
||||
label: t("shortcut.request.share_request"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
|
||||
7
packages/hoppscotch-common/src/pages/e/_id.vue
Normal file
7
packages/hoppscotch-common/src/pages/e/_id.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-between p-8">
|
||||
Temporary page for Embed till the feature is ready
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
@@ -22,14 +22,16 @@
|
||||
</div>
|
||||
<div v-else class="flex flex-1 flex-col items-center justify-center p-4">
|
||||
<div
|
||||
v-if="shortcodeDetails.loading"
|
||||
v-if="sharedRequestDetails.loading"
|
||||
class="flex flex-1 flex-col items-center justify-center p-4"
|
||||
>
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="!shortcodeDetails.loading && E.isLeft(shortcodeDetails.data)"
|
||||
v-if="
|
||||
!sharedRequestDetails.loading && E.isLeft(sharedRequestDetails.data)
|
||||
"
|
||||
class="flex flex-col items-center p-4"
|
||||
>
|
||||
<icon-lucide-alert-triangle class="svg-icons mb-2 opacity-75" />
|
||||
@@ -55,7 +57,10 @@
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="!shortcodeDetails.loading && E.isRight(shortcodeDetails.data)"
|
||||
v-if="
|
||||
!sharedRequestDetails.loading &&
|
||||
E.isRight(sharedRequestDetails.data)
|
||||
"
|
||||
class="flex flex-1 flex-col items-center justify-center p-4"
|
||||
>
|
||||
<h1 class="heading">
|
||||
@@ -95,9 +100,9 @@ const t = useI18n()
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const invalidLink = ref(false)
|
||||
const shortcodeID = ref("")
|
||||
const sharedRequestID = ref("")
|
||||
|
||||
const shortcodeDetails = useGQLQuery<
|
||||
const sharedRequestDetails = useGQLQuery<
|
||||
ResolveShortcodeQuery,
|
||||
ResolveShortcodeQueryVariables,
|
||||
""
|
||||
@@ -109,14 +114,14 @@ const shortcodeDetails = useGQLQuery<
|
||||
})
|
||||
|
||||
watch(
|
||||
() => shortcodeDetails.data,
|
||||
() => sharedRequestDetails.data,
|
||||
() => addRequestToTab()
|
||||
)
|
||||
|
||||
const addRequestToTab = () => {
|
||||
if (shortcodeDetails.loading) return
|
||||
if (sharedRequestDetails.loading) return
|
||||
|
||||
const data = shortcodeDetails.data
|
||||
const data = sharedRequestDetails.data
|
||||
|
||||
if (E.isRight(data)) {
|
||||
if (!data.right.shortcode?.request) {
|
||||
@@ -145,9 +150,9 @@ const reloadApplication = () => {
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof route.params.id === "string") {
|
||||
shortcodeID.value = route.params.id
|
||||
shortcodeDetails.execute()
|
||||
sharedRequestID.value = route.params.id
|
||||
sharedRequestDetails.execute()
|
||||
}
|
||||
invalidLink.value = !shortcodeID.value
|
||||
invalidLink.value = !sharedRequestID.value
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||
import IconWindow from "~icons/lucide/app-window"
|
||||
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||
import IconCode2 from "~icons/lucide/code-2"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import IconFileCode from "~icons/lucide/file-code"
|
||||
import IconRename from "~icons/lucide/file-edit"
|
||||
import IconPlay from "~icons/lucide/play"
|
||||
@@ -94,10 +94,10 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
||||
icon: markRaw(IconRename),
|
||||
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||
},
|
||||
copy_request_link: {
|
||||
text: this.t("shortcut.request.copy_request_link"),
|
||||
alternates: ["copy", "link"],
|
||||
icon: markRaw(IconCopy),
|
||||
share_request: {
|
||||
text: this.t("shortcut.request.share_request"),
|
||||
alternates: ["share", "request", "copy"],
|
||||
icon: markRaw(IconShare2),
|
||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
||||
},
|
||||
reset_request: {
|
||||
@@ -277,8 +277,8 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
||||
case "rename_request":
|
||||
invokeAction("request.rename")
|
||||
break
|
||||
case "copy_request_link":
|
||||
invokeAction("request.copy-link")
|
||||
case "share_request":
|
||||
invokeAction("request.share-request")
|
||||
break
|
||||
case "reset_request":
|
||||
invokeAction("request.reset")
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { defineConfig } from "windicss/helpers"
|
||||
|
||||
export default defineConfig({
|
||||
extract: {
|
||||
include: ["src/**/*.{vue,html}", "../hoppscotch-ui/src/**/*.{vue,html}"],
|
||||
},
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
},
|
||||
extend: {
|
||||
inset: {
|
||||
upperPrimaryStickyFold: "var(--upper-primary-sticky-fold)",
|
||||
upperSecondaryStickyFold: "var(--upper-secondary-sticky-fold)",
|
||||
upperTertiaryStickyFold: "var(--upper-tertiary-sticky-fold)",
|
||||
upperFourthStickyFold: "var(--upper-fourth-sticky-fold)",
|
||||
upperMobilePrimaryStickyFold: "var(--upper-mobile-primary-sticky-fold)",
|
||||
upperMobileSecondaryStickyFold:
|
||||
"var(--upper-mobile-secondary-sticky-fold)",
|
||||
upperMobileStickyFold: "var(--upper-mobile-sticky-fold)",
|
||||
upperMobileTertiaryStickyFold:
|
||||
"var(--upper-mobile-tertiary-sticky-fold)",
|
||||
lowerPrimaryStickyFold: "var(--lower-primary-sticky-fold)",
|
||||
lowerSecondaryStickyFold: "var(--lower-secondary-sticky-fold)",
|
||||
lowerTertiaryStickyFold: "var(--lower-tertiary-sticky-fold)",
|
||||
lowerFourthStickyFold: "var(--lower-fourth-sticky-fold)",
|
||||
sidebarPrimaryStickyFold: "var(--sidebar-primary-sticky-fold)",
|
||||
sidebarSecondaryStickyFold: "var(--line-height-body)",
|
||||
},
|
||||
colors: {
|
||||
primary: "var(--primary-color)",
|
||||
primaryLight: "var(--primary-light-color)",
|
||||
primaryDark: "var(--primary-dark-color)",
|
||||
primaryContrast: "var(--primary-contrast-color)",
|
||||
secondary: "var(--secondary-color)",
|
||||
secondaryLight: "var(--secondary-light-color)",
|
||||
secondaryDark: "var(--secondary-dark-color)",
|
||||
accent: "var(--accent-color)",
|
||||
accentLight: "var(--accent-light-color)",
|
||||
accentDark: "var(--accent-dark-color)",
|
||||
accentContrast: "var(--accent-contrast-color)",
|
||||
divider: "var(--divider-color)",
|
||||
dividerLight: "var(--divider-light-color)",
|
||||
dividerDark: "var(--divider-dark-color)",
|
||||
error: "var(--error-color)",
|
||||
tooltip: "var(--tooltip-color)",
|
||||
popover: "var(--popover-color)",
|
||||
gradientFrom: "var(--gradient-from-color)",
|
||||
gradientVia: "var(--gradient-via-color)",
|
||||
gradientTo: "var(--gradient-to-color)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: "var(--font-sans)",
|
||||
mono: "var(--font-mono)",
|
||||
icon: "var(--font-icon)",
|
||||
},
|
||||
fontSize: {
|
||||
tiny: "var(--font-size-tiny)",
|
||||
body: "var(--font-size-body)",
|
||||
},
|
||||
lineHeight: {
|
||||
body: "var(--line-height-body)",
|
||||
},
|
||||
cursor: {
|
||||
nsResize: "ns-resize",
|
||||
grab: "grab",
|
||||
grabbing: "grabbing",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -16,6 +16,9 @@
|
||||
<label
|
||||
for="checkbox"
|
||||
class="pl-0 font-semibold truncate align-middle cursor-pointer"
|
||||
:class="{
|
||||
'before:mr-2': labelSlot.default
|
||||
}"
|
||||
>
|
||||
<slot></slot>
|
||||
</label>
|
||||
@@ -23,6 +26,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSlots } from 'vue';
|
||||
|
||||
defineProps({
|
||||
on: {
|
||||
type: Boolean,
|
||||
@@ -33,6 +38,9 @@ defineProps({
|
||||
const emit = defineEmits<{
|
||||
(e: "change"): void
|
||||
}>()
|
||||
|
||||
// used to check if the default slot is used and add a margin to the label if exists
|
||||
const labelSlot = useSlots()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -55,7 +63,6 @@ const emit = defineEmits<{
|
||||
@apply h-4;
|
||||
@apply w-4;
|
||||
@apply font-icon;
|
||||
@apply mr-2;
|
||||
@apply transition;
|
||||
@apply content-["\e5ca"];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<HoppSmartRadio v-for="(radio, index) in radios" :key="`radio-${index}`" :value="radio.value" :label="radio.label"
|
||||
:selected="modelValue === radio.value" @change="emit('update:modelValue', radio.value)" />
|
||||
<HoppSmartRadio
|
||||
v-for="(radio, index) in radios"
|
||||
:key="`radio-${index}`"
|
||||
:value="radio.value"
|
||||
:label="radio.label"
|
||||
:selected="modelValue === radio.value"
|
||||
@change="emit('update:modelValue', radio.value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppSmartRadio } from '.';
|
||||
import { HoppSmartRadio } from "."
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: string): void
|
||||
|
||||
Reference in New Issue
Block a user