diff --git a/frontend/src/components/AppComponent.vue b/frontend/src/components/AppComponent.vue index cc282413..3cc97bb7 100644 --- a/frontend/src/components/AppComponent.vue +++ b/frontend/src/components/AppComponent.vue @@ -139,6 +139,7 @@ const boundValue = computed({ }, }) +// events const router = useRouter() const route = useRoute() const componentEvents = computed(() => { diff --git a/frontend/src/components/CodeEditor.vue b/frontend/src/components/CodeEditor.vue index 1d2234bd..f64e0d51 100644 --- a/frontend/src/components/CodeEditor.vue +++ b/frontend/src/components/CodeEditor.vue @@ -133,7 +133,7 @@ const setupEditor = () => { } const getModelValue = () => { - let value = props.modelValue || "" + let value = props.modelValue ?? "" try { if (props.type === "JSON" || typeof value === "object") { value = jsToJson(value) diff --git a/frontend/src/components/CodePanel.vue b/frontend/src/components/CodePanel.vue new file mode 100644 index 00000000..10a65ff5 --- /dev/null +++ b/frontend/src/components/CodePanel.vue @@ -0,0 +1,211 @@ + + + diff --git a/frontend/src/components/ComponentEditor.vue b/frontend/src/components/ComponentEditor.vue index 33e7e6ea..83e556cb 100644 --- a/frontend/src/components/ComponentEditor.vue +++ b/frontend/src/components/ComponentEditor.vue @@ -206,6 +206,7 @@ watchEffect(() => { canvasStore.activeCanvas?.activeBreakpoint canvasStore.dropTarget.placeholder + canvasStore.dropTarget.index canvasStore.activeCanvas?.canvasProps.breakpoints.map((breakpoint) => breakpoint.visible) nextTick(() => { diff --git a/frontend/src/components/ComponentEvents.vue b/frontend/src/components/ComponentEvents.vue index cda77d7a..e3f89340 100644 --- a/frontend/src/components/ComponentEvents.vue +++ b/frontend/src/components/ComponentEvents.vue @@ -92,7 +92,6 @@ import EmptyState from "@/components/EmptyState.vue" import { isObjectEmpty, confirm } from "@/utils/helpers" import { getComponentEvents } from "@/utils/components" -import { useVariables } from "@/utils/useVariables" import { SelectOption } from "@/types" import { Actions, ActionConfigurations, ComponentEvent } from "@/types/ComponentEvent" @@ -138,7 +137,6 @@ const eventOptions = computed(() => { "keypress", ] }) -const { variableOptions } = useVariables(store.variables) const doctypeFields = ref<{ label: string; value: string }[]>([]) watch( @@ -297,7 +295,7 @@ const actions: ActionConfigurations = { label: "Variable", fieldname: "value", fieldtype: "select", - options: variableOptions.value, + options: store.variableOptions, }, ], rows: newEvent.value.fields, diff --git a/frontend/src/components/ComponentProps.vue b/frontend/src/components/ComponentProps.vue index d31698e0..39ba5c72 100644 --- a/frontend/src/components/ComponentProps.vue +++ b/frontend/src/components/ComponentProps.vue @@ -29,7 +29,7 @@ /> @@ -132,7 +132,6 @@ import useStudioStore from "@/stores/studioStore" import IconButton from "@/components/IconButton.vue" import CodeEditor from "@/components/CodeEditor.vue" import blockController from "@/utils/blockController" -import { useVariables } from "@/utils/useVariables" const props = defineProps<{ block?: Block @@ -194,7 +193,6 @@ const getSlotContent = (slot: Slot) => { } // variable binding -const { variableOptions } = useVariables(store.variables) const boundValue = computed({ get() { const modelValue = props.block?.componentProps.modelValue diff --git a/frontend/src/components/DataPanel.vue b/frontend/src/components/DataPanel.vue index 306dac29..a044dc5e 100644 --- a/frontend/src/components/DataPanel.vue +++ b/frontend/src/components/DataPanel.vue @@ -52,11 +52,11 @@ :name="variable_name" class="-ml-[0.9rem] overflow-hidden" /> -
-
{{ variable_name }}
+
+
{{ variable_name }}
(), + { + type: "text", + modelValue: "", + }, +) + const emit = defineEmits(["update:modelValue", "input"]) const data = useVModel(props, "modelValue", emit) diff --git a/frontend/src/components/StudioCanvas.vue b/frontend/src/components/StudioCanvas.vue index bc34720b..9ca4ff30 100644 --- a/frontend/src/components/StudioCanvas.vue +++ b/frontend/src/components/StudioCanvas.vue @@ -43,7 +43,7 @@
+ +
+ +
@@ -75,6 +79,7 @@ import PanelResizer from "@/components/PanelResizer.vue" import ComponentPanel from "@/components/ComponentPanel.vue" import ComponentLayers from "@/components/ComponentLayers.vue" import DataPanel from "@/components/DataPanel.vue" +import CodePanel from "@/components/CodePanel.vue" import IconButton from "@/components/IconButton.vue" import Block from "@/utils/block" diff --git a/frontend/src/data/studioWatchers.ts b/frontend/src/data/studioWatchers.ts new file mode 100644 index 00000000..29588af0 --- /dev/null +++ b/frontend/src/data/studioWatchers.ts @@ -0,0 +1,11 @@ +import { createListResource } from "frappe-ui" + +const studioWatchers = createListResource({ + doctype: "Studio Page Watcher", + parent: "Studio Page", + fields: ["name", "source", "script", "immediate", "parent"], + orderBy: "modified desc", + pageLength: 50, +}) + +export { studioWatchers } \ No newline at end of file diff --git a/frontend/src/pages/AppContainer.vue b/frontend/src/pages/AppContainer.vue index bfb87380..365ec357 100644 --- a/frontend/src/pages/AppContainer.vue +++ b/frontend/src/pages/AppContainer.vue @@ -39,6 +39,7 @@ watch( if (!page.value) return await store.setLocalState({ route: route }) await store.setPageData(page.value) + await store.setPageWatchers(page.value) const blocks = jsonToJs(page.value?.blocks) if (blocks) { rootBlock.value = getBlockInstance(blocks[0]) diff --git a/frontend/src/stores/appStore.ts b/frontend/src/stores/appStore.ts index 9558d1a0..13ad0c75 100644 --- a/frontend/src/stores/appStore.ts +++ b/frontend/src/stores/appStore.ts @@ -1,17 +1,20 @@ import { defineStore } from "pinia" -import { reactive, ref } from "vue" +import { ref, watch, type WatchStopHandle } from "vue" import { studioPageResources } from "@/data/studioResources" import { studioVariables } from "@/data/studioVariables" -import { getInitialVariableValue, getNewResource } from "@/utils/helpers" +import { studioWatchers } from "@/data/studioWatchers" +import { getInitialVariableValue, getNewResource, executeUserScript } from "@/utils/helpers" import type { Resource } from "@/types/Studio/StudioResource" import type { StudioPage } from "@/types/Studio/StudioPage" import type { Variable } from "@/types/Studio/StudioPageVariable" +import type { StudioPageWatcher } from "@/types/Studio/StudioPageWatcher" const useAppStore = defineStore("appStore", () => { const resources = ref>({}) const variables = ref>({}) const localState = ref({}) + const activeWatchers = ref>({}) async function setPageData(page: StudioPage) { await setPageResources(page) @@ -51,6 +54,33 @@ const useAppStore = defineStore("appStore", () => { localState.value = params } + async function setPageWatchers(page: StudioPage) { + cleanupWatchers() + studioWatchers.filters = { parent: page.name } + await studioWatchers.reload() + + studioWatchers.data.map((watcher: StudioPageWatcher) => { + setupWatcher(watcher) + }) + } + + function setupWatcher(watcher: StudioPageWatcher) { + const isDeep = typeof variables.value[watcher.source] === "object" + const watcherFn = watch( + () => variables.value[watcher.source], + () => { + executeUserScript(watcher.script, variables.value, resources.value) + }, + { deep: isDeep, immediate: watcher.immediate } + ) + activeWatchers.value[watcher.name || watcher.source] = watcherFn + } + + function cleanupWatchers() { + Object.values(activeWatchers.value).forEach(stop => stop()) + activeWatchers.value = {} + } + return { setPageData, resources, @@ -59,6 +89,7 @@ const useAppStore = defineStore("appStore", () => { setPageVariables, localState, setLocalState, + setPageWatchers, } }) diff --git a/frontend/src/stores/studioStore.ts b/frontend/src/stores/studioStore.ts index 6f50d3e9..1ecfc84b 100644 --- a/frontend/src/stores/studioStore.ts +++ b/frontend/src/stores/studioStore.ts @@ -1,4 +1,4 @@ -import { ref, reactive, nextTick } from "vue" +import { ref, reactive, nextTick, computed } from "vue" import router from "@/router/studio_router" import { defineStore } from "pinia" @@ -24,7 +24,7 @@ import useCanvasStore from "@/stores/canvasStore" import type { StudioApp } from "@/types/Studio/StudioApp" import type { StudioPage } from "@/types/Studio/StudioPage" import type { Resource } from "@/types/Studio/StudioResource" -import { LeftPanelOptions, RightPanelOptions } from "@/types" +import { LeftPanelOptions, RightPanelOptions, SelectOption } from "@/types" import ComponentContextMenu from "@/components/ComponentContextMenu.vue" import { studioVariables } from "@/data/studioVariables" import { Variable } from "@/types/Studio/StudioPageVariable" @@ -245,6 +245,25 @@ const useStudioStore = defineStore("store", () => { }) } + const variableOptions = computed(() => { + const options: SelectOption[] = [] + + function traverse(obj: any, path = "") { + for (const key in obj) { + const currentPath = path ? `${path}.${key}` : key + options.push({ value: currentPath, label: currentPath }) + + if (typeof obj[key] === "object" && obj[key] !== null) { + // add nested properties + traverse(obj[key], currentPath) + } + } + } + + traverse(variables.value) + return options + }) + return { // layout studioLayout, @@ -280,6 +299,7 @@ const useStudioStore = defineStore("store", () => { setPageData, setPageResources, setPageVariables, + variableOptions, } }) diff --git a/frontend/src/types/Studio/StudioPageWatcher.ts b/frontend/src/types/Studio/StudioPageWatcher.ts new file mode 100644 index 00000000..285d3bd8 --- /dev/null +++ b/frontend/src/types/Studio/StudioPageWatcher.ts @@ -0,0 +1,7 @@ +export type StudioPageWatcher = { + source: string, + script: string, + immediate: boolean, + parent?: string, + name?: string, +} \ No newline at end of file diff --git a/frontend/src/utils/block.ts b/frontend/src/utils/block.ts index 7fbeed30..171832fc 100644 --- a/frontend/src/utils/block.ts +++ b/frontend/src/utils/block.ts @@ -168,13 +168,11 @@ class Block implements BlockOptions { "DatePicker", "DateTimePicker", "DateRangePicker", - "FormControl", "Input", "Select", "Switch", "Textarea", "TextEditor", - "TextInput", // studio components "Audio", "ImageView", diff --git a/frontend/src/utils/components.ts b/frontend/src/utils/components.ts index a5a3c09e..8edda9c2 100644 --- a/frontend/src/utils/components.ts +++ b/frontend/src/utils/components.ts @@ -76,7 +76,7 @@ function getPropEnums(componentName: string, propName: string): string[] | undef /** * fetches prop enums like Button.json > definitions > ButtonProps > properties > variant > enum - ["solid", "subtle", "outline", "ghost"] */ - return componentTypes[componentName]?.definitions?.[`${componentName}Props`]?.properties?.[propName]?.enum + return componentTypes?.[componentName]?.definitions?.[`${componentName}Props`]?.properties?.[propName]?.enum } // events diff --git a/frontend/src/utils/useCanvasDropZone.ts b/frontend/src/utils/useCanvasDropZone.ts index 9f3a8669..0a1f48e4 100644 --- a/frontend/src/utils/useCanvasDropZone.ts +++ b/frontend/src/utils/useCanvasDropZone.ts @@ -1,6 +1,6 @@ import useCanvasStore from "@/stores/canvasStore" import Block from "@/utils/block" -import { getComponentBlock, throttle } from "@/utils/helpers" +import { getComponentBlock } from "@/utils/helpers" import { useDropZone } from "@vueuse/core" import { Ref } from "vue" @@ -131,7 +131,7 @@ export function useCanvasDropZone( return "column" } - const updateDropTarget = throttle(( + const updateDropTarget = ( ev: DragEvent, parentComponent: Block | null, slotName: string | null, @@ -155,13 +155,8 @@ export function useCanvasDropZone( if (canvasStore.dropTarget.parentComponent?.componentId === parentComponent.componentId && canvasStore.dropTarget.index === index) return // flip placeholder border as per layout direction to avoid shifting elements too much - if (layoutDirection === "row") { - placeholder.classList.remove("horizontal-placeholder") - placeholder.classList.add("vertical-placeholder") - } else { - placeholder.classList.remove("vertical-placeholder") - placeholder.classList.add("horizontal-placeholder") - } + placeholder.classList.toggle("vertical-placeholder", layoutDirection === "row") + placeholder.classList.toggle("horizontal-placeholder", layoutDirection === "column") // add the placeholder to the new parent // exclude placeholder as its going to move with this update @@ -177,7 +172,7 @@ export function useCanvasDropZone( canvasStore.dropTarget.slotName = slotName canvasStore.dropTarget.x = ev.x canvasStore.dropTarget.y = ev.y - }, 130) + } return { isOverDropZone } } diff --git a/frontend/src/utils/useVariables.ts b/frontend/src/utils/useVariables.ts deleted file mode 100644 index 4c6bbb49..00000000 --- a/frontend/src/utils/useVariables.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { computed } from "vue" -import { SelectOption } from "@/types" - -export function useVariables(variables: Record) { - const variableOptions = computed(() => { - const options: SelectOption[] = [] - - function traverse(obj: any, path = "") { - for (const key in obj) { - const currentPath = path ? `${path}.${key}` : key - options.push({ value: currentPath, label: currentPath }) - - if (typeof obj[key] === "object" && obj[key] !== null) { - // add nested properties - traverse(obj[key], currentPath) - } - } - } - - traverse(variables) - return options - }) - - return { - variableOptions - } -} \ No newline at end of file diff --git a/studio/studio/doctype/studio_client_script/__init__.py b/studio/studio/doctype/studio_client_script/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/studio/studio/doctype/studio_client_script/studio_client_script.js b/studio/studio/doctype/studio_client_script/studio_client_script.js new file mode 100644 index 00000000..e7d97feb --- /dev/null +++ b/studio/studio/doctype/studio_client_script/studio_client_script.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Studio Client Script", { +// refresh(frm) { + +// }, +// }); diff --git a/studio/studio/doctype/studio_client_script/studio_client_script.json b/studio/studio/doctype/studio_client_script/studio_client_script.json new file mode 100644 index 00000000..c01ed83c --- /dev/null +++ b/studio/studio/doctype/studio_client_script/studio_client_script.json @@ -0,0 +1,47 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "prompt", + "creation": "2025-03-31 13:56:13.627801", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "script" + ], + "fields": [ + { + "fieldname": "script", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Script", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-03-31 14:02:02.257570", + "modified_by": "Administrator", + "module": "Studio", + "name": "Studio Client Script", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/studio/studio/doctype/studio_client_script/studio_client_script.py b/studio/studio/doctype/studio_client_script/studio_client_script.py new file mode 100644 index 00000000..ad20c475 --- /dev/null +++ b/studio/studio/doctype/studio_client_script/studio_client_script.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class StudioClientScript(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + script: DF.Code + # end: auto-generated types + + pass diff --git a/studio/studio/doctype/studio_client_script/test_studio_client_script.py b/studio/studio/doctype/studio_client_script/test_studio_client_script.py new file mode 100644 index 00000000..6f977eec --- /dev/null +++ b/studio/studio/doctype/studio_client_script/test_studio_client_script.py @@ -0,0 +1,29 @@ +# Copyright (c) 2025, Frappe Technologies Pvt Ltd and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestStudioClientScript(UnitTestCase): + """ + Unit tests for StudioClientScript. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestStudioClientScript(IntegrationTestCase): + """ + Integration tests for StudioClientScript. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/studio/studio/doctype/studio_page/studio_page.json b/studio/studio/doctype/studio_page/studio_page.json index d274a37e..cfad6c19 100644 --- a/studio/studio/doctype/studio_page/studio_page.json +++ b/studio/studio/doctype/studio_page/studio_page.json @@ -17,7 +17,11 @@ "draft_blocks", "data_tab", "resources", - "variables" + "variables", + "scripts_tab", + "watchers", + "section_break_fmxt", + "client_scripts" ], "fields": [ { @@ -89,12 +93,34 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Studio App", - "options": "Studio App" + "options": "Studio App", + "search_index": 1 + }, + { + "fieldname": "scripts_tab", + "fieldtype": "Tab Break", + "label": "Scripts" + }, + { + "fieldname": "client_scripts", + "fieldtype": "Table MultiSelect", + "label": "Client Scripts", + "options": "Studio Page Client Script" + }, + { + "fieldname": "watchers", + "fieldtype": "Table", + "label": "Watchers", + "options": "Studio Page Watcher" + }, + { + "fieldname": "section_break_fmxt", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-03-05 16:53:24.306451", + "modified": "2025-04-01 09:33:28.358540", "modified_by": "Administrator", "module": "Studio", "name": "Studio Page", @@ -114,9 +140,10 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "title_field": "page_title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/studio/studio/doctype/studio_page/studio_page.py b/studio/studio/doctype/studio_page/studio_page.py index 7f036e40..4407f012 100644 --- a/studio/studio/doctype/studio_page/studio_page.py +++ b/studio/studio/doctype/studio_page/studio_page.py @@ -16,10 +16,15 @@ class StudioPage(Document): if TYPE_CHECKING: from frappe.types import DF + from studio.studio.doctype.studio_page_client_script.studio_page_client_script import ( + StudioPageClientScript, + ) from studio.studio.doctype.studio_page_resource.studio_page_resource import StudioPageResource from studio.studio.doctype.studio_page_variable.studio_page_variable import StudioPageVariable + from studio.studio.doctype.studio_page_watcher.studio_page_watcher import StudioPageWatcher blocks: DF.JSON | None + client_scripts: DF.TableMultiSelect[StudioPageClientScript] draft_blocks: DF.JSON | None page_name: DF.Data | None page_title: DF.Data | None @@ -28,6 +33,7 @@ class StudioPage(Document): route: DF.Data | None studio_app: DF.Link | None variables: DF.Table[StudioPageVariable] + watchers: DF.Table[StudioPageWatcher] # end: auto-generated types def autoname(self): diff --git a/studio/studio/doctype/studio_page_client_script/__init__.py b/studio/studio/doctype/studio_page_client_script/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/studio/studio/doctype/studio_page_client_script/studio_page_client_script.json b/studio/studio/doctype/studio_page_client_script/studio_page_client_script.json new file mode 100644 index 00000000..3496a4b8 --- /dev/null +++ b/studio/studio/doctype/studio_page_client_script/studio_page_client_script.json @@ -0,0 +1,35 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-03-31 14:02:29.684581", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "studio_script" + ], + "fields": [ + { + "fieldname": "studio_script", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Studio Script", + "options": "Studio Client Script", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-03-31 14:03:08.236395", + "modified_by": "Administrator", + "module": "Studio", + "name": "Studio Page Client Script", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/studio/studio/doctype/studio_page_client_script/studio_page_client_script.py b/studio/studio/doctype/studio_page_client_script/studio_page_client_script.py new file mode 100644 index 00000000..7eea6da4 --- /dev/null +++ b/studio/studio/doctype/studio_page_client_script/studio_page_client_script.py @@ -0,0 +1,23 @@ +# Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class StudioPageClientScript(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + studio_script: DF.Link + # end: auto-generated types + + pass diff --git a/studio/studio/doctype/studio_page_watcher/__init__.py b/studio/studio/doctype/studio_page_watcher/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/studio/studio/doctype/studio_page_watcher/studio_page_watcher.js b/studio/studio/doctype/studio_page_watcher/studio_page_watcher.js new file mode 100644 index 00000000..3138b619 --- /dev/null +++ b/studio/studio/doctype/studio_page_watcher/studio_page_watcher.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Studio Page Watcher", { +// refresh(frm) { + +// }, +// }); diff --git a/studio/studio/doctype/studio_page_watcher/studio_page_watcher.json b/studio/studio/doctype/studio_page_watcher/studio_page_watcher.json new file mode 100644 index 00000000..43707874 --- /dev/null +++ b/studio/studio/doctype/studio_page_watcher/studio_page_watcher.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-04-01 09:31:00.594704", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "source", + "script", + "immediate" + ], + "fields": [ + { + "fieldname": "source", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Source", + "max_height": "100px", + "reqd": 1 + }, + { + "fieldname": "script", + "fieldtype": "Code", + "in_list_view": 1, + "label": "Script", + "reqd": 1 + }, + { + "default": "0", + "description": "By default, this script won't run unless the Source value changes. Enable this to run the script immediately.", + "fieldname": "immediate", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Run Immediately?" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-04-01 13:57:15.006423", + "modified_by": "Administrator", + "module": "Studio", + "name": "Studio Page Watcher", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/studio/studio/doctype/studio_page_watcher/studio_page_watcher.py b/studio/studio/doctype/studio_page_watcher/studio_page_watcher.py new file mode 100644 index 00000000..a5aa1571 --- /dev/null +++ b/studio/studio/doctype/studio_page_watcher/studio_page_watcher.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025, Frappe Technologies Pvt Ltd and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class StudioPageWatcher(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + immediate: DF.Check + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + script: DF.Code + source: DF.Code + # end: auto-generated types + + pass diff --git a/studio/studio/doctype/studio_page_watcher/test_studio_page_watcher.py b/studio/studio/doctype/studio_page_watcher/test_studio_page_watcher.py new file mode 100644 index 00000000..8afbfba1 --- /dev/null +++ b/studio/studio/doctype/studio_page_watcher/test_studio_page_watcher.py @@ -0,0 +1,29 @@ +# Copyright (c) 2025, Frappe Technologies Pvt Ltd and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestStudioPageWatcher(UnitTestCase): + """ + Unit tests for StudioPageWatcher. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestStudioPageWatcher(IntegrationTestCase): + """ + Integration tests for StudioPageWatcher. + Use this class for testing interactions between multiple components. + """ + + pass