Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ed0bc97
feat: open scripts in external editor
pratikb64 May 5, 2026
d1a13c1
feat: add open in external editor in context menu
pratikb64 May 7, 2026
571efc1
Merge remote-tracking branch 'upstream/develop' into feat/open-script…
pratikb64 May 11, 2026
cbfed1c
refactor: change extension uri
pratikb64 May 11, 2026
3049ad6
fix: editor label in context menu
pratikb64 May 12, 2026
f1c1c7c
Merge remote-tracking branch 'upstream/develop' into feat/open-script…
pratikb64 May 12, 2026
8b9abbc
fix: handle local network access popup gracefully
pratikb64 May 13, 2026
2e7267f
Merge remote-tracking branch 'upstream/develop' into feat/open-script…
pratikb64 May 26, 2026
39a0af1
fix: remove variable files
pratikb64 May 26, 2026
3d6a233
feat: add realtime script sync
pratikb64 Jun 1, 2026
4189d0d
fi: realtime sync enablement
pratikb64 Jun 2, 2026
1079d12
Merge remote-tracking branch 'upstream/develop' into feat/open-script…
pratikb64 Jun 2, 2026
9cd249d
fix: recursive unsubscribe event bug
pratikb64 Jun 2, 2026
b7f4b72
refactor: realtime sync logic
pratikb64 Jun 2, 2026
d1546f2
revert: builder variable deletion
pratikb64 Jun 2, 2026
953fc8f
fix: restore variables using git
pratikb64 Jun 2, 2026
e9db9fc
Merge branch 'develop' into feat/open-scripts-in-external-editor
mergify[bot] Jun 6, 2026
f1ee784
Merge branch 'develop' into feat/open-scripts-in-external-editor
mergify[bot] Jun 6, 2026
0af7255
Merge remote-tracking branch 'upstream/develop' into feat/open-script…
pratikb64 Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ builder/public/page_scripts
builder/public/page_styles
**/www/_builder.html
builder/public/dist
codedb.snapshot
3 changes: 3 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<script setup lang="ts">
import useBuilderStore from "@/stores/builderStore";
import usePageStore from "@/stores/pageStore";
import { useBuilderSettingsSync } from "@/composables/useLiveDocSync";
import { UseDark } from "@vueuse/components";
import { useTitle } from "@vueuse/core";
import { FrappeUIProvider } from "frappe-ui";
Expand All @@ -24,6 +25,8 @@ const builderStore = useBuilderStore();
const pageStore = usePageStore();
const route = useRoute();

useBuilderSettingsSync();

provide("sessionUser", sessionUser);

const title = computed(() => {
Expand Down
42 changes: 39 additions & 3 deletions frontend/src/components/BlockContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ import { promptCreateComponent } from "@/utils/dialogs";
import useBuilderStore from "@/stores/builderStore";
import useCanvasStore from "@/stores/canvasStore";
import useComponentStore from "@/stores/componentStore";
import usePageStore from "@/stores/pageStore";
import getBlockTemplate from "@/utils/blockTemplate";
import { confirm, detachBlockFromComponent, getBlockCopy, triggerCopyEvent } from "@/utils/helpers";
import { useStorage } from "@vueuse/core";
import { Ref, inject, nextTick, ref } from "vue";
import { Ref, inject, nextTick, ref, computed } from "vue";
import { useExternalEditor, createEditorContext } from "@/composables/useExternalEditor";
import { toast } from "frappe-ui";

const builderStore = useBuilderStore();
const componentStore = useComponentStore();
const canvasStore = useCanvasStore();
const pageStore = usePageStore();
const { isExternalEditorActive, openInExternalEditor, editorName } = useExternalEditor();

const contextMenu = ref(null) as unknown as Ref<InstanceType<typeof ContextMenu>>;
const triggeredFromLayersPanel = ref(false);
Expand Down Expand Up @@ -58,11 +62,33 @@ const pasteStyle = () => {
block.value.updateStyles(copiedStyle.value?.style as BlockStyleObjects);
};

const openScriptInExternalEditor = async (scriptType: "blockClientScript" | "blockDataScript") => {
if (!block.value.blockId) return;

const context = createEditorContext(
"Builder Page",
pageStore.selectedPage || pageStore.pageName,
undefined,
block.value.blockId,
scriptType,
);

if (!context) return;

const result = await openInExternalEditor(context);
const scriptName = scriptType === "blockClientScript" ? "Client Script" : "Data Script";
if (!result.success) {
toast.error(result.error || `Failed to open ${scriptName.toLowerCase()} in ${editorName.value}`);
} else {
toast.success(`${scriptName} opened in ${editorName.value}`);
}
};

const duplicateBlock = () => {
block.value.duplicateBlock();
};

const contextMenuOptions: ContextMenuOption[] = [
const contextMenuOptions = computed((): ContextMenuOption[] => [
{
label: "Edit with AI",
action: () => {
Expand Down Expand Up @@ -258,6 +284,16 @@ const contextMenuOptions: ContextMenuOption[] = [
condition: () => block.value.isExtendedFromComponent(),
disabled: () => builderStore.readOnlyMode,
},
{
label: `Open Client Script in ${editorName.value}`,
action: () => openScriptInExternalEditor("blockClientScript"),
condition: () => isExternalEditorActive.value && !block.value.isRoot(),
},
{
label: `Open Data Script in ${editorName.value}`,
action: () => openScriptInExternalEditor("blockDataScript"),
condition: () => isExternalEditorActive.value && !block.value.isRoot(),
},
{
label: "Save as Block Template",
action: () => {
Expand Down Expand Up @@ -324,7 +360,7 @@ const contextMenuOptions: ContextMenuOption[] = [
},
disabled: () => builderStore.readOnlyMode,
},
];
]);

defineExpose({
showContextMenu,
Expand Down
44 changes: 38 additions & 6 deletions frontend/src/components/Controls/CodeEditor.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
<template>
<div class="code-editor relative flex flex-col gap-1">
<span class="text-p-sm-medium text-ink-gray-8" v-show="label">
{{ label }}
<span v-if="isDirty" class="text-[10px] text-gray-600">●</span>
</span>
<div
class="code-editor relative flex flex-col gap-1"
:class="{
'-mt-6': isExternalEditorActive && !label,
}">
<div class="flex items-center justify-between">
<div>
<span class="text-p-sm font-medium text-ink-gray-8" v-show="label">
{{ label }}
<span v-if="isDirty" class="text-[10px] text-gray-600">●</span>
</span>
</div>
<Button
v-if="isExternalEditorActive && externalEditorContext"
@click="handleOpenInExternalEditor"
variant="ghost"
size="sm"
class="!gap-1 text-p-xs"
icon-right="arrow-up-right">
{{ `Open in ${editorName}` }}
</Button>
</div>
<div
:style="{
'min-height': height,
Expand All @@ -25,7 +42,7 @@
<Button
@click="actionButton?.handler"
variant="subtle"
class="!h-6 !w-6 border !border-outline-gray-2 bg-surface-base [&>svg]:!h-3.5 [&>svg]:!w-3.5"
class="bg-surface-base !h-6 !w-6 border !border-outline-gray-2 [&>svg]:!h-3.5 [&>svg]:!w-3.5"
:icon="actionButton.icon"
:title="actionButton.label"
:disabled="readonly"></Button>
Expand All @@ -44,6 +61,8 @@
<script setup lang="ts">
import { ref, VNodeRef, watch } from "vue";
import CodeMirrorEditor from "./CodeMirror/CodeMirrorEditor.vue";
import { useExternalEditor, type OpenScriptRequest } from "@/composables/useExternalEditor";
import { toast } from "frappe-ui";

const props = withDefaults(
defineProps<{
Expand All @@ -62,6 +81,7 @@ const props = withDefaults(
icon: string;
handler: () => void;
};
externalEditorContext?: OpenScriptRequest;
}>(),
{
type: "JSON",
Expand All @@ -75,6 +95,18 @@ const props = withDefaults(
},
);

const { isExternalEditorActive, openInExternalEditor, editorName } = useExternalEditor();

const handleOpenInExternalEditor = async () => {
if (!props.externalEditorContext) return;
const result = await openInExternalEditor(props.externalEditorContext);
if (!result.success) {
toast.error(result.error || `Failed to open in ${editorName.value}`);
} else {
toast.success(`Code opened in ${editorName.value}`);
}
};

const emit = defineEmits(["save", "update:modelValue"]);
const editor = ref<VNodeRef | null>(null);

Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/PageClientScriptManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@
:autofocus="false"
:show-save-button="true"
@save="updateScript"
:show-line-numbers="true"></CodeEditor>
:show-line-numbers="true"
:external-editor-context="getEditorContext()"></CodeEditor>
</div>
</div>
</template>
Expand All @@ -140,6 +141,7 @@ import draggable from "vuedraggable";
import CodeEditor from "./Controls/CodeEditor.vue";
import CSSIcon from "./Icons/CSS.vue";
import JavaScriptIcon from "./Icons/JavaScript.vue";
import { createEditorContext } from "@/composables/useExternalEditor";

const { capture } = useTelemetry();

Expand Down Expand Up @@ -196,6 +198,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;

Expand Down
17 changes: 14 additions & 3 deletions frontend/src/components/PageScript.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<div v-else class="mt-2 text-center text-sm text-ink-gray-6">Select a block to edit script</div>
</div>
<div
class="absolute bottom-0 left-0 box-border flex w-full items-center justify-between border-t bg-surface-base p-4 py-2">
class="bg-surface-base absolute bottom-0 left-0 box-border flex w-full items-center justify-between border-t p-4 py-2">
<h2 class="text-base text-ink-gray-6">Mode</h2>
<TabButtons
class="w-fit"
Expand Down Expand Up @@ -65,13 +65,14 @@
:autofocus="true"
@save="savePageDataScript"
:showSaveButton="true"
:show-line-numbers="true"></CodeEditor>
:show-line-numbers="true"
:external-editor-context="getPageEditorContext('page_data_script')"></CodeEditor>
<CodeEditor
v-model="pageStore.pageData"
type="JSON"
label="Data Preview"
:showLineNumbers="true"
class="-mt-5 w-1/3 [&>div>div]:bg-surface-base"
class="[&>div>div]:bg-surface-base -mt-5 w-1/3"
height="calc(100% - 110px)"
description='Use Data Script to provide dynamic data to your web page.<br>
<b>Example:</b> data.events = frappe.get_list("Event")<br><br>
Expand Down Expand Up @@ -117,6 +118,7 @@ import CodeEditor from "./Controls/CodeEditor.vue";
import TabButtons from "./Controls/TabButtons.vue";
import PageClientScriptManager from "./PageClientScriptManager.vue";
import PropsEditor from "./PropsEditor.vue";
import { createEditorContext } from "@/composables/useExternalEditor";
import useCanvasStore from "@/stores/canvasStore.js";

const { capture } = useTelemetry();
Expand Down Expand Up @@ -151,6 +153,15 @@ const blockClientScript = computed(() => {
return "";
});

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({
Expand Down
17 changes: 13 additions & 4 deletions frontend/src/components/Settings/GlobalCode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
height="100px"
class="shrink-0"
@update:modelValue="builderStore.updateBuilderSettings('head_html', $event)"
:showLineNumbers="true"></CodeEditor>
:showLineNumbers="true"
:externalEditorContext="getEditorContext('head_html')"></CodeEditor>
<CodeEditor
label="<body> HTML"
type="HTML"
Expand All @@ -17,7 +18,8 @@
height="100px"
class="shrink-0"
@update:modelValue="builderStore.updateBuilderSettings('body_html', $event)"
:showLineNumbers="true"></CodeEditor>
:showLineNumbers="true"
:externalEditorContext="getEditorContext('body_html')"></CodeEditor>
<CodeEditor
label="Client Script"
type="JavaScript"
Expand All @@ -26,7 +28,8 @@
height="100px"
class="shrink-0"
@update:modelValue="(code) => builderStore.updateBuilderSettings('script', code)"
:showLineNumbers="true"></CodeEditor>
:showLineNumbers="true"
:externalEditorContext="getEditorContext('script')"></CodeEditor>
<CodeEditor
label="Style"
type="CSS"
Expand All @@ -35,13 +38,19 @@
height="100px"
class="shrink-0"
@update:modelValue="(code) => builderStore.updateBuilderSettings('style', code)"
:showLineNumbers="true"></CodeEditor>
:showLineNumbers="true"
:externalEditorContext="getEditorContext('style')"></CodeEditor>
</div>
</template>
<script setup lang="ts">
import CodeEditor from "@/components/Controls/CodeEditor.vue";
import { builderSettings } from "@/data/builderSettings";
import useBuilderStore from "@/stores/builderStore";
import { createEditorContext } from "@/composables/useExternalEditor";

const builderStore = useBuilderStore();

const getEditorContext = (field: string) => {
return createEditorContext("Builder Settings", "Builder Settings", field);
};
</script>
53 changes: 46 additions & 7 deletions frontend/src/components/Settings/GlobalDeveloper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Execute Block Client Scripts in Editor
</label>
<Select
class="!w-[200px]"
class="h-max !w-[200px]"
:modelValue="builderSettings.doc?.execute_block_scripts_in_editor"
@update:modelValue="
(value) => builderStore.updateBuilderSettings('execute_block_scripts_in_editor', value)
Expand All @@ -26,18 +26,57 @@
builderStore.updateBuilderSettings('restrict_click_handlers', val);
}
" />
<div class="flex flex-col gap-2">
<p class="text-p-sm text-ink-gray-7">
Note: Block Scripts are executed in a sandboxed environment. This may have limitations and might not
perfectly replicate live site behavior. Executing untrusted scripts could be unsafe.
</p>
<div class="flex justify-between gap-x-2.5">
<div class="flex flex-col gap-1">
<label class="text-p-base font-medium text-ink-gray-8">Integrate with External Editor</label>
<span v-if="lnaPermissionStatus === 'denied'" class="inline text-p-sm text-ink-gray-7">
Follow this
<a
href="https://docs.frappe.io/builder/external-editor"
target="_blank"
class="text-ink-blue-6 text-p-sm underline"
v-text="'guide'"></a>
to enable local network access.
</span>
<p v-else class="text-p-sm text-ink-gray-7">
Allow Builder to access your local network for integration with external editors like VS Code. Click
<a
href="https://docs.frappe.io/builder/external-editor"
target="_blank"
class="text-ink-blue-6 text-p-sm underline"
v-text="'here'"></a>
for more information.
</p>
</div>
<div class="flex shrink-0 gap-3">
<div v-if="['granted', 'denied', 'unsupported'].includes(lnaPermissionStatus)">
<span v-if="lnaPermissionStatus === 'granted'" class="text-p-sm text-ink-green-3">
Access Granted
</span>
<span v-else-if="lnaPermissionStatus === 'denied'" class="text-p-sm text-ink-red-4">
Access Denied
</span>
<span v-else-if="lnaPermissionStatus === 'unsupported'" class="text-p-sm text-ink-gray-7">
Unsupported
</span>
</div>
<Button
v-if="lnaPermissionStatus === 'prompt'"
@click="requestLocalNetworkAccess"
:loading="isRequestingAccess"
variant="subtle">
Request Access
</Button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { builderSettings } from "@/data/builderSettings";
import useBuilderStore from "@/stores/builderStore";
import { Select, Switch } from "frappe-ui";
import { Button, Select } from "frappe-ui";
import { useExternalEditor } from "@/composables/useExternalEditor";

const builderStore = useBuilderStore();
const { lnaPermissionStatus, isRequestingAccess, requestLocalNetworkAccess } = useExternalEditor();
</script>
Loading
Loading