Skip to content

Commit 5fb2752

Browse files
committed
copy to clipboard, and url decode for : and , to keep it human readable
1 parent 475e22f commit 5fb2752

File tree

2 files changed

+135
-2
lines changed

2 files changed

+135
-2
lines changed

packages/api-client/src/components/AddressBar/AddressBar.vue

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import { ServerDropdown } from '@/components/Server'
1616
import { useLayout } from '@/hooks'
1717
import { useWorkspace } from '@/store'
1818
import type { EnvVariable } from '@/store/active-entities'
19+
import { useActiveEntities } from '@/store/active-entities'
20+
import { createFetchQueryParams } from '@/libs/send-request/create-fetch-query-params'
21+
import { replaceTemplateVariables } from '@/libs/string-template'
22+
import { getApiKeyForUrl, doesUrlRequireApiKey } from '@/libs/api-key-manager'
23+
import { mergeUrls } from '@scalar/helpers/url/merge-urls'
1924
2025
import HttpMethod from '../HttpMethod/HttpMethod.vue'
2126
import AddressBarHistory from './AddressBarHistory.vue'
@@ -37,6 +42,7 @@ defineEmits<{
3742
const id = useId()
3843
3944
const { requestMutators, events } = useWorkspace()
45+
const { activeExample } = useActiveEntities()
4046
4147
const { layout } = useLayout()
4248
@@ -160,6 +166,105 @@ events.hotKeys.on((event) => {
160166
function updateRequestPath(url: string) {
161167
requestMutators.edit(operation.uid, 'path', url)
162168
}
169+
170+
/**
171+
* Decode specific URL-encoded characters to make URLs more readable
172+
* Whitelisted characters that should be decoded back to original form
173+
*/
174+
const decodeWhitelistedCharacters = (url: string): string => {
175+
return url
176+
.replace(/%3A/g, ':') // Decode colons
177+
.replace(/%2C/g, ',') // Decode commas
178+
}
179+
180+
/** Get the complete URL including server, query params, and API key injection */
181+
function getCompleteUrl(): string {
182+
try {
183+
const env = environment || {}
184+
185+
// Get the current active example with user-entered parameter values
186+
if (!activeExample.value) {
187+
// Fallback to basic URL construction if no active example
188+
const serverString = replaceTemplateVariables(server?.url ?? '', env)
189+
const pathString = replaceTemplateVariables(operation.path, env)
190+
let url = serverString || pathString
191+
192+
if (!url) return ''
193+
194+
// Get API key injection
195+
const resolvedWorkspaceId = workspace.uid || 'default'
196+
const apiKey = getApiKeyForUrl(resolvedWorkspaceId, serverString)
197+
const shouldInjectApiKey = doesUrlRequireApiKey(serverString)
198+
199+
return decodeWhitelistedCharacters((mergeUrls as any)(url, pathString, new URLSearchParams(), false, apiKey || undefined, shouldInjectApiKey))
200+
}
201+
202+
/** Parsed and evaluated values for path parameters */
203+
const pathVariables = activeExample.value.parameters.path.reduce<Record<string, string>>((vars, param) => {
204+
if (param.enabled) {
205+
vars[param.key] = replaceTemplateVariables(param.value, env)
206+
}
207+
return vars
208+
}, {})
209+
210+
const serverString = replaceTemplateVariables(server?.url ?? '', env)
211+
// Replace environment variables, then path variables
212+
const pathString = replaceTemplateVariables(replaceTemplateVariables(operation.path, env), pathVariables)
213+
214+
/**
215+
* Start building the main URL, we cannot use the URL class yet as it does not work with relative servers
216+
* Also handles the case of no server with pathString
217+
*/
218+
let url = serverString || pathString
219+
220+
// Handle empty url
221+
if (!url) {
222+
return ''
223+
}
224+
225+
// Set the server variables (for now we only support default values)
226+
Object.entries(server?.variables ?? {}).forEach(([k, v]) => {
227+
url = replaceTemplateVariables(url, {
228+
[k]: pathVariables[k] || v.default,
229+
})
230+
})
231+
232+
// Create query parameters from the current example
233+
const urlParams = createFetchQueryParams(activeExample.value, env, operation)
234+
235+
// Get API key for this workspace if it's needed for the server URL
236+
const resolvedWorkspaceId = workspace.uid || 'default'
237+
const apiKey = getApiKeyForUrl(resolvedWorkspaceId, serverString)
238+
const shouldInjectApiKey = doesUrlRequireApiKey(serverString)
239+
240+
// Combine the url with the path and server + query params + API key injection
241+
const finalUrl = (mergeUrls as any)(url, pathString, urlParams, false, apiKey || undefined, shouldInjectApiKey)
242+
243+
return decodeWhitelistedCharacters(finalUrl)
244+
} catch (error) {
245+
console.error('Error building complete URL:', error)
246+
// Fallback to basic URL construction
247+
return decodeWhitelistedCharacters(`${server?.url || ''}${operation.path || ''}`)
248+
}
249+
}
250+
251+
/** Copy the complete URL to clipboard */
252+
async function copyUrlToClipboard() {
253+
try {
254+
const url = getCompleteUrl()
255+
await navigator.clipboard.writeText(url)
256+
// TODO: Add toast notification for success
257+
} catch (error) {
258+
console.error('Failed to copy URL to clipboard:', error)
259+
// Fallback for older browsers
260+
const textArea = document.createElement('textarea')
261+
textArea.value = getCompleteUrl()
262+
document.body.appendChild(textArea)
263+
textArea.select()
264+
document.execCommand('copy')
265+
document.body.removeChild(textArea)
266+
}
267+
}
163268
</script>
164269
<template>
165270
<div
@@ -228,6 +333,20 @@ function updateRequestPath(url: string) {
228333
<AddressBarHistory
229334
:operation="operation"
230335
:target="id" />
336+
337+
<!-- Copy URL Button -->
338+
<ScalarButton
339+
class="z-context-plus relative h-auto shrink-0 overflow-hidden p-2"
340+
title="Copy complete URL to clipboard"
341+
variant="ghost"
342+
@click="copyUrlToClipboard">
343+
<ScalarIcon
344+
class="relative shrink-0 fill-current"
345+
icon="Clipboard"
346+
size="xs" />
347+
<span class="sr-only">Copy complete URL to clipboard</span>
348+
</ScalarButton>
349+
231350
<ScalarButton
232351
ref="sendButtonRef"
233352
class="z-context-plus relative h-auto shrink-0 overflow-hidden py-1 pr-2.5 pl-2 font-bold"

packages/api-client/src/views/Components/CodeSnippet/helpers/get-snippet.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
import type { ErrorResponse } from '@/libs/errors'
22
import { type ClientId, type TargetId, snippetz } from '@scalar/snippetz'
33
import type { Request as HarRequest } from 'har-format'
4+
import { getApiKeyValue } from '@/libs/api-key-manager'
45

56
/** Key used to hack around the invalid urls */
67
const INVALID_URLS_PREFIX = 'ws://replace.me'
78

9+
/**
10+
* Decode specific URL-encoded characters to make URLs more readable
11+
* Whitelisted characters that should be decoded back to original form
12+
*/
13+
const decodeWhitelistedCharacters = (url: string): string => {
14+
return url
15+
.replace(/%3A/g, ':') // Decode colons
16+
.replace(/%2C/g, ',') // Decode commas
17+
}
18+
819
const injectApiKeyPlaceholder = (url: string) => {
920
if (url.startsWith('https://pro-api.llama.fi')) {
1021
const urlParts = new URL(url)
11-
return `${urlParts.origin}/<API-KEY>${urlParts.pathname}${urlParts.search}`
22+
const actualApiKey = getApiKeyValue('default')
23+
const keyToInject = actualApiKey || '<API-KEY>'
24+
const finalUrl = `${urlParts.origin}/${keyToInject}${urlParts.pathname}${urlParts.search}`
25+
return decodeWhitelistedCharacters(finalUrl)
1226
}
13-
return url
27+
return decodeWhitelistedCharacters(url)
1428
}
1529

1630
/**

0 commit comments

Comments
 (0)