From ed0bc979cd0bc936e465f890cf5721c5c5ab5a76 Mon Sep 17 00:00:00 2001 From: Pratik Date: Tue, 5 May 2026 17:01:12 +0530 Subject: [PATCH 01/12] 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, }; } From 39a0af1b5e4a7fa04df5b249777e468ca215a833 Mon Sep 17 00:00:00 2001 From: Pratik Date: Tue, 26 May 2026 11:38:11 +0530 Subject: [PATCH 06/12] fix: remove variable files --- .../builder_variable/gray_200/gray_200.json | 14 -------------- .../builder_variable/gray_400/gray_400.json | 14 -------------- .../builder_variable/gray_500/gray_500.json | 14 -------------- .../builder_variable/gray_600/gray_600.json | 14 -------------- .../builder_variable/gray_700/gray_700.json | 14 -------------- 5 files changed, 70 deletions(-) delete mode 100644 builder/builder/builder_variable/gray_200/gray_200.json delete mode 100644 builder/builder/builder_variable/gray_400/gray_400.json delete mode 100644 builder/builder/builder_variable/gray_500/gray_500.json delete mode 100644 builder/builder/builder_variable/gray_600/gray_600.json delete mode 100644 builder/builder/builder_variable/gray_700/gray_700.json diff --git a/builder/builder/builder_variable/gray_200/gray_200.json b/builder/builder/builder_variable/gray_200/gray_200.json deleted file mode 100644 index 2b359c58e..000000000 --- a/builder/builder/builder_variable/gray_200/gray_200.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "creation": "2025-07-03 20:30:29.822822", - "docstatus": 0, - "doctype": "Builder Variable", - "idx": 0, - "is_standard": 1, - "modified": "2025-07-04 15:19:30.412835", - "modified_by": "Administrator", - "name": "gray-200", - "owner": "Administrator", - "variable_name": "gray-200", - "type": "Color", - "value": "#EDEDED" -} diff --git a/builder/builder/builder_variable/gray_400/gray_400.json b/builder/builder/builder_variable/gray_400/gray_400.json deleted file mode 100644 index cf8e8ea4f..000000000 --- a/builder/builder/builder_variable/gray_400/gray_400.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "creation": "2025-07-03 20:30:01.677360", - "docstatus": 0, - "doctype": "Builder Variable", - "idx": 0, - "is_standard": 1, - "modified": "2025-07-04 15:19:30.425007", - "modified_by": "Administrator", - "name": "gray-400", - "owner": "Administrator", - "variable_name": "gray-400", - "type": "Color", - "value": "#C7C7C7" -} diff --git a/builder/builder/builder_variable/gray_500/gray_500.json b/builder/builder/builder_variable/gray_500/gray_500.json deleted file mode 100644 index cf836c08e..000000000 --- a/builder/builder/builder_variable/gray_500/gray_500.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "creation": "2025-07-03 20:29:40.804845", - "docstatus": 0, - "doctype": "Builder Variable", - "idx": 0, - "is_standard": 1, - "modified": "2025-07-04 15:19:30.429585", - "modified_by": "Administrator", - "name": "gray-500", - "owner": "Administrator", - "variable_name": "gray-500", - "type": "Color", - "value": "#999999" -} diff --git a/builder/builder/builder_variable/gray_600/gray_600.json b/builder/builder/builder_variable/gray_600/gray_600.json deleted file mode 100644 index d6579e67f..000000000 --- a/builder/builder/builder_variable/gray_600/gray_600.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "creation": "2025-07-03 20:29:18.003824", - "docstatus": 0, - "doctype": "Builder Variable", - "idx": 0, - "is_standard": 1, - "modified": "2025-07-04 15:19:30.436442", - "modified_by": "Administrator", - "name": "gray-600", - "owner": "Administrator", - "variable_name": "gray-600", - "type": "Color", - "value": "#7C7C7C" -} diff --git a/builder/builder/builder_variable/gray_700/gray_700.json b/builder/builder/builder_variable/gray_700/gray_700.json deleted file mode 100644 index 397d8b229..000000000 --- a/builder/builder/builder_variable/gray_700/gray_700.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "creation": "2025-07-03 20:28:52.451410", - "docstatus": 0, - "doctype": "Builder Variable", - "idx": 0, - "is_standard": 1, - "modified": "2025-07-04 15:19:30.455261", - "modified_by": "Administrator", - "name": "gray-700", - "owner": "Administrator", - "variable_name": "gray-700", - "type": "Color", - "value": "#525252" -} From 3d6a233f2b32647400e0c514caa5b274997184fb Mon Sep 17 00:00:00 2001 From: Pratik Date: Tue, 2 Jun 2026 00:10:37 +0530 Subject: [PATCH 07/12] feat: add realtime script sync --- .gitignore | 1 + frontend/components.d.ts | 1 - .../src/components/Controls/CodeEditor.vue | 12 ++- .../src/composables/useRealtimeDocSync.ts | 84 +++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 frontend/src/composables/useRealtimeDocSync.ts diff --git a/.gitignore b/.gitignore index 3e19a5565..212b223f3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ builder/public/page_scripts builder/public/page_styles builder/www/_builder.html builder/public/dist +codedb.snapshot \ No newline at end of file diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 1418d20c0..d2429f933 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -98,7 +98,6 @@ declare module 'vue' { Input: typeof import('./src/components/Controls/Input.vue')['default'] InputLabel: typeof import('./src/components/Controls/InputLabel.vue')['default'] JavaScript: typeof import('./src/components/Icons/JavaScript.vue')['default'] - KeyboardShortcutsModal: typeof import('./src/components/KeyboardShortcutsModal.vue')['default'] Layers: typeof import('./src/components/Icons/Layers.vue')['default'] Loading: typeof import('./src/components/Icons/Loading.vue')['default'] MainMenu: typeof import('./src/components/MainMenu.vue')['default'] diff --git a/frontend/src/components/Controls/CodeEditor.vue b/frontend/src/components/Controls/CodeEditor.vue index edb1200b2..e90b9e45f 100644 --- a/frontend/src/components/Controls/CodeEditor.vue +++ b/frontend/src/components/Controls/CodeEditor.vue @@ -59,10 +59,11 @@