@@ -16,6 +16,11 @@ import { ServerDropdown } from '@/components/Server'
16
16
import { useLayout } from ' @/hooks'
17
17
import { useWorkspace } from ' @/store'
18
18
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'
19
24
20
25
import HttpMethod from ' ../HttpMethod/HttpMethod.vue'
21
26
import AddressBarHistory from ' ./AddressBarHistory.vue'
@@ -37,6 +42,7 @@ defineEmits<{
37
42
const id = useId ()
38
43
39
44
const { requestMutators, events } = useWorkspace ()
45
+ const { activeExample } = useActiveEntities ()
40
46
41
47
const { layout } = useLayout ()
42
48
@@ -160,6 +166,105 @@ events.hotKeys.on((event) => {
160
166
function updateRequestPath(url : string ) {
161
167
requestMutators .edit (operation .uid , ' path' , url )
162
168
}
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
+ }
163
268
</script >
164
269
<template >
165
270
<div
@@ -228,6 +333,20 @@ function updateRequestPath(url: string) {
228
333
<AddressBarHistory
229
334
:operation =" operation"
230
335
: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
+
231
350
<ScalarButton
232
351
ref =" sendButtonRef"
233
352
class =" z-context-plus relative h-auto shrink-0 overflow-hidden py-1 pr-2.5 pl-2 font-bold"
0 commit comments