From ed0bc979cd0bc936e465f890cf5721c5c5ab5a76 Mon Sep 17 00:00:00 2001 From: Pratik Date: Tue, 5 May 2026 17:01:12 +0530 Subject: [PATCH 1/5] feat: open scripts in external editor --- .../src/components/Controls/CodeEditor.vue | 42 ++++++- .../components/PageClientScriptManager.vue | 8 +- frontend/src/components/PageScript.vue | 23 +++- .../src/components/Settings/GlobalCode.vue | 17 ++- frontend/src/components/Settings/PageCode.vue | 11 +- frontend/src/composables/useExternalEditor.ts | 118 ++++++++++++++++++ 6 files changed, 202 insertions(+), 17 deletions(-) create mode 100644 frontend/src/composables/useExternalEditor.ts diff --git a/frontend/src/components/Controls/CodeEditor.vue b/frontend/src/components/Controls/CodeEditor.vue index 6ef989fe8..edb1200b2 100644 --- a/frontend/src/components/Controls/CodeEditor.vue +++ b/frontend/src/components/Controls/CodeEditor.vue @@ -1,9 +1,26 @@ @@ -146,6 +147,7 @@ import CodeEditor from "./Controls/CodeEditor.vue"; import CSSIcon from "./Icons/CSS.vue"; import GripVertical from "./Icons/GripVertical.vue"; import JavaScriptIcon from "./Icons/JavaScript.vue"; +import { createEditorContext } from "@/composables/useExternalEditor"; const { capture } = useTelemetry(); @@ -207,6 +209,10 @@ const selectScript = (script: attachedScript) => { }); }; +const getEditorContext = () => { + return createEditorContext("Builder Client Script", activeScript.value?.script_name, "script"); +}; + const updateScript = (value: string) => { if (!activeScript.value || builderStore.readOnlyMode) return; diff --git a/frontend/src/components/PageScript.vue b/frontend/src/components/PageScript.vue index b0555e4de..99e6ff759 100644 --- a/frontend/src/components/PageScript.vue +++ b/frontend/src/components/PageScript.vue @@ -79,7 +79,8 @@ :autofocus="true" @save="savePageDataScript" :showSaveButton="true" - :show-line-numbers="true"> + :show-line-numbers="true" + :external-editor-context="getPageEditorContext('page_data_script')"> + Example:
this.addEventListener("click", () => { console.log(props) })


+ For more details on how to write data script, refer to this documentation.'>
@@ -126,7 +128,8 @@ :autofocus="true" @save="saveBlockDataScript" :showSaveButton="true" - :show-line-numbers="true"> + :show-line-numbers="true" + :external-editor-context="getBlockEditorContext('blockDataScript')">
{ ? blockDataStore.getBlockData( blockController.getFirstSelectedBlock().blockId, showInheritedBlockData.value ? "all" : "own", - ) || {} + ) || {} : {}; }); +const getPageEditorContext = (field: string) => { + return createEditorContext("Builder Page", props.page?.name, field); +}; + +const getBlockEditorContext = (blockField: "blockClientScript" | "blockDataScript") => { + const block = blockController.getFirstSelectedBlock(); + return createEditorContext("Builder Page", props.page?.name, undefined, block?.blockId, blockField); +}; + const savePageDataScript = (value: string) => { webPages.setValue .submit({ diff --git a/frontend/src/components/Settings/GlobalCode.vue b/frontend/src/components/Settings/GlobalCode.vue index 403afc286..06631d828 100644 --- a/frontend/src/components/Settings/GlobalCode.vue +++ b/frontend/src/components/Settings/GlobalCode.vue @@ -8,7 +8,8 @@ height="100px" class="shrink-0" @update:modelValue="builderStore.updateBuilderSettings('head_html', $event)" - :showLineNumbers="true"> + :showLineNumbers="true" + :externalEditorContext="getEditorContext('head_html')"> + :showLineNumbers="true" + :externalEditorContext="getEditorContext('body_html')"> + :showLineNumbers="true" + :externalEditorContext="getEditorContext('script')"> + :showLineNumbers="true" + :externalEditorContext="getEditorContext('style')">
diff --git a/frontend/src/components/Settings/PageCode.vue b/frontend/src/components/Settings/PageCode.vue index 71ce45e83..a23019cf9 100644 --- a/frontend/src/components/Settings/PageCode.vue +++ b/frontend/src/components/Settings/PageCode.vue @@ -9,7 +9,8 @@ class="shrink-0" :modelValue="pageStore.activePage?.head_html" @update:modelValue="(val) => pageStore.updateActivePage('head_html', val)" - :showLineNumbers="true"> + :showLineNumbers="true" + :externalEditorContext="getEditorContext('head_html')"> + :showLineNumbers="true" + :externalEditorContext="getEditorContext('body_html')">
diff --git a/frontend/src/composables/useExternalEditor.ts b/frontend/src/composables/useExternalEditor.ts index 53737b563..c60aac138 100644 --- a/frontend/src/composables/useExternalEditor.ts +++ b/frontend/src/composables/useExternalEditor.ts @@ -2,6 +2,8 @@ import { ref, onMounted } from "vue"; const EXTERNAL_EDITOR_PORT_RANGE = { start: 59000, end: 59021 }; +type PermissionState = "granted" | "denied" | "prompt" | "unsupported"; + interface ExternalEditorStatus { active: boolean; extension: string; @@ -22,15 +24,35 @@ const isExternalEditorActive = ref(false); const externalEditorPort = ref(null); const externalEditorUriScheme = ref("vscode"); const editorName = ref("VS Code"); +const lnaPermissionStatus = ref("prompt"); +const isRequestingAccess = ref(false); -async function checkExternalEditorStatus(): Promise { - isExternalEditorActive.value = false; - externalEditorPort.value = null; +async function checkLocalNetworkAccess(): Promise { + try { + const result = await navigator.permissions.query({ + name: "local-network-access" as PermissionName, + }); + lnaPermissionStatus.value = result.state as PermissionState; + result.addEventListener("change", () => { + lnaPermissionStatus.value = result.state as PermissionState; + }); + } catch { + lnaPermissionStatus.value = "unsupported"; + } +} + +async function scanPorts( + options: { + timeout?: number; + validateResponse?: (data: ExternalEditorStatus) => boolean; + } = {}, +): Promise<{ port: number; data?: ExternalEditorStatus } | null> { + const { timeout = 500, validateResponse } = options; for (let port = EXTERNAL_EDITOR_PORT_RANGE.start; port <= EXTERNAL_EDITOR_PORT_RANGE.end; port++) { try { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 500); + const timeoutId = setTimeout(() => controller.abort(), timeout); const response = await fetch(`http://127.0.0.1:${port}/status`, { method: "GET", @@ -41,17 +63,39 @@ async function checkExternalEditorStatus(): Promise { if (response.ok) { const data = (await response.json()) as ExternalEditorStatus; - if (data.active && data.extension === "frappe-script-editor") { - isExternalEditorActive.value = true; - externalEditorPort.value = port; - externalEditorUriScheme.value = data.uriScheme || "vscode"; - editorName.value = data.name; - return; + if (!validateResponse || validateResponse(data)) { + return { port, data }; } } - } catch { - // Port not available or timeout, continue to next - } + } catch {} + } + return null; +} + +async function requestLocalNetworkAccess(): Promise { + isRequestingAccess.value = true; + try { + await scanPorts({ timeout: 300 }); + } catch {} + await checkLocalNetworkAccess(); + isRequestingAccess.value = false; +} + +async function checkExternalEditorStatus(): Promise { + isExternalEditorActive.value = false; + externalEditorPort.value = null; + + if (lnaPermissionStatus.value === "denied") return; + + const result = await scanPorts({ + validateResponse: (data) => data.active && data.extension === "frappe-script-editor", + }); + + if (result) { + isExternalEditorActive.value = true; + externalEditorPort.value = result.port; + externalEditorUriScheme.value = result.data?.uriScheme || "vscode"; + editorName.value = result.data?.name || "VS Code"; } } @@ -85,14 +129,21 @@ async function openInExternalEditor( } export function useExternalEditor() { - onMounted(() => { - checkExternalEditorStatus(); + onMounted(async () => { + await checkLocalNetworkAccess(); + if (lnaPermissionStatus.value === "granted" || import.meta.env.DEV) { + await checkExternalEditorStatus(); + } }); return { isExternalEditorActive, openInExternalEditor, editorName, + lnaPermissionStatus, + isRequestingAccess, + requestLocalNetworkAccess, + checkLocalNetworkAccess, }; }