chore: display platform links in app header (#5091)

This commit is contained in:
Nivedin 2025-05-21 21:03:58 +05:30 committed by GitHub
parent 130facb440
commit 9371457d0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 240 additions and 17 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 24 24" fill="#ffffff"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" fill="#606060"/></svg>

After

Width:  |  Height:  |  Size: 680 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 24 24" fill="#E2E8F0"><title>Windows</title><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801" fill="#606060"/></svg>

After

Width:  |  Height:  |  Size: 253 B

View file

@ -118,6 +118,13 @@
"add_pfx_or_pkcs_file": "Add PFX/PKCS12 File"
},
"app": {
"additional_links": {
"macOS": "macOS",
"windows": "Windows",
"linux": "Linux",
"web_app": "Web App",
"cli": "CLI"
},
"chat_with_us": "Chat with us",
"contact_us": "Contact us",
"cookies": "Cookies",

View file

@ -59,14 +59,47 @@
class="col-span-2 flex items-center justify-between space-x-2"
>
<div class="flex">
<HoppButtonSecondary
v-if="showInstallButton"
v-tippy="{ theme: 'tooltip' }"
:title="t('header.install_pwa')"
:icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="installPWA()"
/>
<tippy
v-if="
kernelMode === 'web' &&
downloadableLinks &&
downloadableLinks.length > 0
"
interactive
trigger="click"
theme="popover"
:on-shown="() => downloadableLinksRef.focus()"
>
<HoppButtonSecondary
:icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
<template #content="{ hide }">
<div
ref="downloadableLinksRef"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<template v-for="link in downloadableLinks" :key="link.id">
<HoppButtonSecondary
v-if="link.show ?? true"
:icon="link.icon"
:label="link.text(t)"
:blank="true"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark justify-between"
:to="
link.action.type === 'link' ? link.action.href : undefined
"
@click="
link.action.type === 'custom' ? link.action.do() : null
"
/>
</template>
</div>
</template>
</tippy>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${
@ -318,7 +351,6 @@ import { getKernelMode } from "@hoppscotch/kernel"
import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream"
import { defineActionHandler, invokeAction } from "@helpers/actions"
import { installPWA, pwaDefferedPrompt } from "@modules/pwa"
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
@ -344,6 +376,7 @@ import IconUserPlus from "~icons/lucide/user-plus"
import IconUsers from "~icons/lucide/users"
import IconChevronDown from "~icons/lucide/chevron-down"
import IconLayoutDashboard from "~icons/lucide/layout-dashboard"
import { AdditionalLinksService } from "~/services/additionalLinks.service"
const t = useI18n()
const toast = useToast()
@ -352,6 +385,8 @@ const instanceSwitcherService =
kernelMode === "desktop" ? useService(InstanceSwitcherService) : null
const instanceSwitcherRef =
kernelMode === "desktop" ? ref<any | null>(null) : ref(null)
const downloadableLinksRef =
kernelMode === "web" ? ref<any | null>(null) : ref(null)
const isUserAdmin = ref(false)
@ -380,14 +415,6 @@ const workspaceSelectorFlagEnabled = computed(
() => !!platform.platformFeatureFlags.workspaceSwitcherLogin?.value
)
/**
* Once the PWA code is initialized, this holds a method
* that can be called to show the user the installation
* prompt.
*/
const showInstallButton = computed(() => !!pwaDefferedPrompt.value)
/**
* Show the dashboard link if the user is not on the default cloud instance and is an admin
*/
@ -420,6 +447,24 @@ const offlineBanner: BannerContent = {
dismissible: true,
}
const additionalLinks = useService(AdditionalLinksService)
platform.additionalLinks?.forEach((linkSet) => {
useService(linkSet)
})
const downloadableLinks = computed(() => {
if (kernelMode !== "web") return null
const headerDownloadableLink = additionalLinks?.getLinkSet(
"HEADER_DOWNLOADABLE_LINKS"
)
if (!headerDownloadableLink) return null
return headerDownloadableLink.getLinks().value
})
// Show the offline banner if the app is offline
const network = reactive(useNetwork())
const isOnline = computed(() => network.isOnline)

View file

@ -0,0 +1,8 @@
import { Container, ServiceClassInstance } from "dioc"
import { AdditionalLinksService } from "~/services/additionalLinks.service"
export type AdditionalLinksPlatformDef = Array<
ServiceClassInstance<unknown> & {
new (c: Container): AdditionalLinksService
}
>

View file

@ -20,6 +20,7 @@ import { UIPlatformDef } from "./ui"
import { BackendPlatformDef } from "./backend"
import { OrganizationPlatformDef } from "./organization"
import { KernelIO } from "./kernel-io"
import { AdditionalLinksPlatformDef } from "./additionalLinks"
export type PlatformDef = {
ui?: UIPlatformDef
@ -69,6 +70,7 @@ export type PlatformDef = {
experiments?: ExperimentsPlatformDef
backend: BackendPlatformDef
organization?: OrganizationPlatformDef
additionalLinks?: AdditionalLinksPlatformDef
}
export let platform: PlatformDef

View file

@ -0,0 +1,55 @@
import { Service } from "dioc"
import { Component, ComputedRef, Ref } from "vue"
import { getI18n } from "~/modules/i18n"
export type Link = {
id: string
text: (t: ReturnType<typeof getI18n>) => string
icon: Component
action: { type: "link"; href: string } | { type: "custom"; do: () => void }
show?: ComputedRef<boolean>
}
// The possible links IDs
type LinkID = "HEADER_DOWNLOADABLE_LINKS" | string
export interface AdditionalLinkSet {
linkSetID: LinkID
getLinks: () => Ref<Link[]>
}
/**
* Service to manage additional links in the app
* This service is used to register and retrieve link sets
* that can be displayed in the app's UI.
* Each link set can contain multiple links, each with its own action.
* The links can be displayed in different parts of the app, such as the header or footer.
*/
export class AdditionalLinksService extends Service {
public static readonly ID = "ADDITIONAL_LINKS_SERVICE"
private additionalLinkSets: Map<string, AdditionalLinkSet> = new Map()
/**
* Registers a link set with the LinksService
* @param linkSet The link set to register
*/
public registerAdditionalSet(linkSet: AdditionalLinkSet) {
this.additionalLinkSets.set(linkSet.linkSetID, linkSet)
}
/**
* Gets all registered link sets
*/
public getAllLinkSets(): IterableIterator<[string, AdditionalLinkSet]> {
return this.additionalLinkSets.entries()
}
/**
* Gets a link set by its ID
* @param linkSetID The ID of the link set to get
*/
public getLinkSet(linkSetID: LinkID): AdditionalLinkSet | undefined {
return this.additionalLinkSets.get(linkSetID)
}
}

View file

@ -26,6 +26,7 @@ import { AgentKernelInterceptorService } from "@hoppscotch/common/platform/std/k
import { ProxyKernelInterceptorService } from "@hoppscotch/common/platform/std/kernel-interceptors/proxy"
import { ExtensionKernelInterceptorService } from "@hoppscotch/common/platform/std/kernel-interceptors/extension"
import { BrowserKernelInterceptorService } from "@hoppscotch/common/platform/std/kernel-interceptors/browser"
import { HeaderDownloadableLinksService } from "./services/headerDownloadableLinks.service"
type Platform = "web" | "desktop"
@ -98,6 +99,7 @@ async function initApp() {
},
infra: InfraPlatform,
backend: stdBackendDef,
additionalLinks: [HeaderDownloadableLinksService],
})
if (kernelMode === "desktop") {

View file

@ -0,0 +1,101 @@
import { Service } from "dioc"
import { computed, markRaw, Ref, ref } from "vue"
import { installPWA, pwaDefferedPrompt } from "@hoppscotch/common/modules/pwa"
import IconGlobe from "~icons/lucide/globe"
import IconCLI from "~icons/lucide/square-terminal"
import IconApple from "~icons/hopp/apple"
import IconWindows from "~icons/hopp/windows"
import IconLinux from "~icons/hopp/linux"
import {
AdditionalLinkSet,
AdditionalLinksService,
Link,
} from "@hoppscotch/common/services/additionalLinks.service"
const macOS: Link = {
id: "whats-new",
text: (t) => t("app.additional_links.macOS"),
icon: markRaw(IconApple),
action: {
type: "link",
href: "https://hoppscotch.com/download?platform=macOS",
},
}
const windows: Link = {
id: "windows",
text: (t) => t("app.additional_links.windows"),
icon: markRaw(IconWindows),
action: {
type: "link",
href: "https://hoppscotch.com/download?platform=windows",
},
}
const linux: Link = {
id: "linux",
text: (t) => t("app.additional_links.linux"),
icon: markRaw(IconLinux),
action: {
type: "link",
href: "https://hoppscotch.com/download?platform=linux",
},
}
const pwa: Link = {
id: "pwa",
text: (t) => t("app.additional_links.web_app"),
icon: IconGlobe,
action: {
type: "custom",
do: () => {
installPWA()
},
},
show: computed(() => !!pwaDefferedPrompt.value),
}
const cli: Link = {
id: "cli",
text: (t) => t("app.additional_links.cli"),
icon: IconCLI,
action: {
type: "link",
href: "https://docs.hoppscotch.io/documentation/clients/cli/overview",
},
}
/**
* Service to manage the downloadable links in the app header.
*/
export class HeaderDownloadableLinksService
extends Service
implements AdditionalLinkSet
{
public static readonly ID = "HEADER_DOWNLOADABLE_LINKS_SERVICE"
public readonly linkSetID = "HEADER_DOWNLOADABLE_LINKS"
private readonly additionalLinkSet = this.bind(AdditionalLinksService)
/**
* List of downloadable links to be shown in the header
* This includes showing the link to the desktop app, PWA, CLI.
*/
private headerDownloadableLinks = ref<Link[]>([
macOS,
windows,
linux,
pwa,
cli,
])
override onServiceInit() {
this.additionalLinkSet.registerAdditionalSet(this)
}
getLinks(): Ref<Link[]> {
// @ts-expect-error show type not recognizing ComputedRef
return this.headerDownloadableLinks
}
}