feat(common): URL encode/decode context menu actions (#5782)
Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
ff906b7c96
commit
2ad2f46e6a
2 changed files with 91 additions and 10 deletions
|
|
@ -379,7 +379,9 @@
|
|||
"context_menu": {
|
||||
"add_parameters": "Add to parameters",
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
"set_environment_variable": "Set as variable",
|
||||
"encode_uri_component": "Encode URL component",
|
||||
"decode_uri_component": "Decode URL component"
|
||||
},
|
||||
"cookies": {
|
||||
"modal": {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
|||
import { getI18n } from "~/modules/i18n"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import IconCopyPlus from "~icons/lucide/copy-plus"
|
||||
import IconLock from "~icons/lucide/lock"
|
||||
import IconUnlock from "~icons/lucide/unlock"
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuResult,
|
||||
|
|
@ -11,22 +13,43 @@ import {
|
|||
ContextMenuState,
|
||||
} from ".."
|
||||
|
||||
/**
|
||||
* Checks if a string matches a URL via the URL constructor or a regex fallback.
|
||||
* The URL constructor rejects endpoints like "localhost:3000" (no protocol),
|
||||
* so the regex covers common patterns without a scheme.
|
||||
*/
|
||||
function checkURL(url: string): boolean {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch {
|
||||
return /^(https?:\/\/)?([\w.-]+)(\.[\w.-]+)+([/?].*)?$/.test(url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check if a string is a valid URL
|
||||
* @param url The string to check
|
||||
* @returns Whether the string is a valid URL
|
||||
*/
|
||||
function isValidURL(url: string) {
|
||||
try {
|
||||
// Try to create a URL object
|
||||
// this will fail for endpoints like "localhost:3000", ie without a protocol
|
||||
new URL(url)
|
||||
return true
|
||||
} catch (_error) {
|
||||
// Fallback to regular expression check
|
||||
const pattern = /^(https?:\/\/)?([\w.-]+)(\.[\w.-]+)+([/?].*)?$/
|
||||
return pattern.test(url)
|
||||
if (checkURL(url)) return true
|
||||
|
||||
// Iteratively decode percent-encoded strings so that encode/decode
|
||||
// round-trips work across multiple levels of encoding.
|
||||
let current = url
|
||||
for (let i = 0; i < 10 && current.includes("%"); i++) {
|
||||
try {
|
||||
const decoded = decodeURIComponent(current)
|
||||
if (decoded === current) break
|
||||
if (checkURL(decoded)) return true
|
||||
current = decoded
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export class URLMenuService extends Service implements ContextMenu {
|
||||
|
|
@ -61,6 +84,31 @@ export class URLMenuService extends Service implements ContextMenu {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the selected text in the current endpoint with encoded/decoded version
|
||||
* @param selectedText The selected text to replace
|
||||
* @param replacement The replacement text (encoded or decoded)
|
||||
*/
|
||||
private replaceSelectedText(selectedText: string, replacement: string) {
|
||||
const currentTab = this.restTab.currentActiveTab.value
|
||||
|
||||
if (!currentTab || currentTab.document.type !== "request") {
|
||||
return
|
||||
}
|
||||
|
||||
const endpoint = currentTab.document.request.endpoint
|
||||
|
||||
// Find and replace the selected text in the endpoint
|
||||
const index = endpoint.indexOf(selectedText)
|
||||
if (index === -1) return
|
||||
|
||||
const newEndpoint =
|
||||
endpoint.slice(0, index) +
|
||||
replacement +
|
||||
endpoint.slice(index + selectedText.length)
|
||||
currentTab.document.request.endpoint = newEndpoint
|
||||
}
|
||||
|
||||
getMenuFor(text: Readonly<string>): ContextMenuState {
|
||||
const results = ref<ContextMenuResult[]>([])
|
||||
|
||||
|
|
@ -77,6 +125,37 @@ export class URLMenuService extends Service implements ContextMenu {
|
|||
this.openNewTab(text)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "encode-url",
|
||||
text: {
|
||||
type: "text",
|
||||
text: this.t("context_menu.encode_uri_component"),
|
||||
},
|
||||
icon: markRaw(IconLock),
|
||||
action: () => {
|
||||
const encoded = encodeURIComponent(text)
|
||||
this.replaceSelectedText(text, encoded)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "decode-url",
|
||||
text: {
|
||||
type: "text",
|
||||
text: this.t("context_menu.decode_uri_component"),
|
||||
},
|
||||
icon: markRaw(IconUnlock),
|
||||
action: () => {
|
||||
try {
|
||||
const decoded = decodeURIComponent(text)
|
||||
this.replaceSelectedText(text, decoded)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[URLMenuService] Failed to decode URI component:",
|
||||
error
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue