Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 39 additions & 3 deletions frontend/src/components/BlockContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ import NewComponent from "@/components/Modals/NewComponent.vue";
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 { toast } from "vue-sonner";
import { useExternalEditor, createEditorContext } from "@/composables/useExternalEditor";

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 @@ -60,11 +64,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 @@ -251,6 +277,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 @@ -317,7 +353,7 @@ const contextMenuOptions: ContextMenuOption[] = [
},
disabled: () => builderStore.readOnlyMode,
},
];
]);

defineExpose({
showContextMenu,
Expand Down
42 changes: 37 additions & 5 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 font-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>
<BuilderButton
v-if="isExternalEditorActive && externalEditorContext"
@click="handleOpenInExternalEditor"
variant="ghost"
size="sm"
class="!gap-1 text-p-xs"
icon-right="arrow-up-right">
{{ `Open in ${editorName}` }}
</BuilderButton>
</div>
<div
:style="{
'min-height': height,
Expand Down Expand Up @@ -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 "vue-sonner";

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 @@ -126,7 +126,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 @@ -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();

Expand Down Expand Up @@ -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;

Expand Down
23 changes: 18 additions & 5 deletions frontend/src/components/PageScript.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
: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"
Expand Down Expand Up @@ -109,9 +110,10 @@
@save="saveBlockClientScript"
:showSaveButton="true"
:show-line-numbers="true"
:external-editor-context="getBlockEditorContext('blockClientScript')"
description='Use Block Client Script to add interactivity to your block. You can access the current DOM node using the keyword `this`. All Block props are accessible using the read-only `props` object.<br>
<b>Example:</b> <pre style="display:inline; font-size: 11px;">this.addEventListener("click", () => { console.log(props) })</pre><br><br>
For more details on how to write data script, refer to <b><a class="underline" href="https://docs.frappe.io/builder/data-script" target="_blank">this documentation</a></b>.'></CodeEditor>
<b>Example:</b> <pre style="display:inline; font-size: 11px;">this.addEventListener("click", () => { console.log(props) })</pre><br><br>
For more details on how to write data script, refer to <b><a class="underline" href="https://docs.frappe.io/builder/data-script" target="_blank">this documentation</a></b>.'></CodeEditor>
</div>
<div v-else>
<div class="flex gap-4">
Expand All @@ -126,7 +128,8 @@
:autofocus="true"
@save="saveBlockDataScript"
:showSaveButton="true"
:show-line-numbers="true"></CodeEditor>
:show-line-numbers="true"
:external-editor-context="getBlockEditorContext('blockDataScript')"></CodeEditor>
<div class="-mt-5 w-1/3 p-4" height="calc(100% - 110px)">
<CodeEditor
v-model="blockData"
Expand Down Expand Up @@ -171,6 +174,7 @@ import Switch from "./Controls/Switch.vue";
import TabButtons from "./Controls/TabButtons.vue";
import PageClientScriptManager from "./PageClientScriptManager.vue";
import PropsEditor from "./PropsEditor.vue";
import { createEditorContext } from "@/composables/useExternalEditor";

const { capture } = useTelemetry();

Expand Down Expand Up @@ -213,10 +217,19 @@ const blockData = computed(() => {
? 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({
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>
70 changes: 58 additions & 12 deletions frontend/src/components/Settings/GlobalDeveloper.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
<template>
<div class="flex flex-col gap-5">
<div class="flex justify-between">
<label class="w-fit shrink-0 text-p-base font-medium text-ink-gray-8">
Execute Block Client Scripts in Editor
</label>
<div class="flex justify-between gap-x-2.5">
<div class="flex flex-col gap-1">
<label class="w-fit shrink-0 text-p-base font-medium text-ink-gray-8">
Execute Block Client Scripts in Editor
</label>
<div class="flex flex-col gap-2">
<p class="text-p-sm text-ink-gray-7">
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>
</div>
<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,20 +34,58 @@
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 Switch from "@/components/Controls/Switch.vue";
import { builderSettings } from "@/data/builderSettings";
import useBuilderStore from "@/stores/builderStore";
import { Select } from "frappe-ui";
import InlineInput from "../Controls/InlineInput.vue";
import { useExternalEditor } from "@/composables/useExternalEditor";
import { Button, Select } from "frappe-ui";

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