@@ -16,6 +16,11 @@ import { ServerDropdown } from '@/components/Server'
1616import { useLayout } from ' @/hooks'
1717import { useWorkspace } from ' @/store'
1818import 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
2025import HttpMethod from ' ../HttpMethod/HttpMethod.vue'
2126import AddressBarHistory from ' ./AddressBarHistory.vue'
@@ -37,6 +42,7 @@ defineEmits<{
3742const id = useId ()
3843
3944const { requestMutators, events } = useWorkspace ()
45+ const { activeExample } = useActiveEntities ()
4046
4147const { layout } = useLayout ()
4248
@@ -160,6 +166,105 @@ events.hotKeys.on((event) => {
160166function 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"
0 commit comments