Skip to content
Merged
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
1 change: 1 addition & 0 deletions frontend/src/components/AppComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const boundValue = computed({
},
})

// events
const router = useRouter()
const route = useRoute()
const componentEvents = computed(() => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/CodeEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
211 changes: 211 additions & 0 deletions frontend/src/components/CodePanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<template>
<CollapsibleSection sectionName="Watchers">
<div class="flex flex-col gap-1">
<div
v-if="studioPageWatchers.data?.length"
v-for="watcher in studioPageWatchers.data"
:key="watcher.name"
class="group/variable flex flex-row justify-between"
>
<div class="flex flex-row justify-between">
<div class="font-mono text-xs font-semibold text-pink-700">{{ watcher.source }}</div>
</div>
<div
class="invisible -mt-1 ml-auto self-start text-gray-600 group-hover/variable:visible has-[.active-item]:visible"
>
<Dropdown :options="getWatcherMenu(watcher)" trigger="click">
<template v-slot="{ open }">
<button
class="flex cursor-pointer items-center rounded-sm p-1 text-gray-700 hover:bg-gray-300"
:class="open ? 'active-item' : ''"
>
<FeatherIcon name="more-horizontal" class="h-3 w-3" />
</button>
</template>
</Dropdown>
</div>
</div>
<EmptyState v-else message="No watchers added" />
</div>

<div class="mt-2 flex flex-col">
<Button icon-left="plus" @click="showWatcherDialog = true">Add Watcher</Button>
<Dialog
v-model="showWatcherDialog"
:options="{
title: pageWatcher.name ? 'Edit Watcher' : 'Add Watcher',
size: '2xl',
}"
@after-leave="
() => {
pageWatcher = {
source: '',
script: '',
immediate: false,
parent: '',
name: '',
}
}
"
:disableOutsideClickToClose="true"
>
<template #body-content>
<div class="flex flex-col space-y-4">
<FormControl
type="autocomplete"
:options="store.variableOptions"
label="Source"
placeholder="Select variable"
:modelValue="pageWatcher.source"
@update:modelValue="
(selectedOption: SelectOption) => {
pageWatcher.source = selectedOption.value
}
"
/>
<CodeEditor
label="Script"
type="JavaScript"
height="250px"
:showLineNumbers="true"
v-model="pageWatcher.script"
/>
<FormControl
type="checkbox"
label="Run Immediately?"
description="By default, this script won't run unless the source value changes. Enable this to run the script immediately."
v-model="pageWatcher.immediate"
/>
</div>
</template>
<template #actions>
<Button
variant="solid"
:label="pageWatcher.name ? 'Update' : 'Add'"
@click="
() => {
if (pageWatcher.name) {
editPageWatcher(pageWatcher)
} else {
addPageWatcher(pageWatcher)
}
}
"
class="w-full"
/>
</template>
</Dialog>
</div>
</CollapsibleSection>
</template>

<script setup lang="ts">
import { ref } from "vue"
import { createListResource } from "frappe-ui"
import EmptyState from "@/components/EmptyState.vue"
import CollapsibleSection from "@/components/CollapsibleSection.vue"
import CodeEditor from "@/components/CodeEditor.vue"
import { StudioPage } from "@/types/Studio/StudioPage"
import { SelectOption } from "@/types"
import { StudioPageWatcher } from "@/types/Studio/StudioPageWatcher"
import useStudioStore from "@/stores/studioStore"
import FormControl from "frappe-ui/src/components/FormControl.vue"
import { toast } from "vue-sonner"
import { confirm } from "@/utils/helpers"

const props = defineProps<{
page: StudioPage
}>()

const studioPageWatchers = createListResource({
doctype: "Studio Page Watcher",
parent: "Studio Page",
filters: {
parent: props.page.name,
},
fields: ["name", "source", "script", "immediate", "parent"],
orderBy: "modified desc",
pageLength: 50,
auto: true,
})

const showWatcherDialog = ref(false)
const pageWatcher = ref<StudioPageWatcher>({
source: "",
script: "",
immediate: false,
parent: "",
name: "",
})
const store = useStudioStore()

const getWatcherMenu = (watcher: StudioPageWatcher) => {
return [
{
label: "Edit",
icon: "edit",
onClick: async () => {
pageWatcher.value = { ...watcher }
showWatcherDialog.value = true
},
},
{
label: "Delete",
icon: "trash",
onClick: () => deletePageWatcher(watcher),
},
]
}

const addPageWatcher = (watcher: StudioPageWatcher) => {
studioPageWatchers.insert.submit(
{
source: watcher.source,
script: watcher.script,
immediate: watcher.immediate,
parent: props.page.name,
parenttype: "Studio Page",
parentfield: "watchers",
},
{
onSuccess() {
showWatcherDialog.value = false
},
onError(error: any) {
toast.error("Failed to add the watcher", {
description: error.messages.join(", "),
})
},
},
)
}

const editPageWatcher = (watcher: StudioPageWatcher) => {
studioPageWatchers.setValue
.submit({
name: watcher.name,
source: watcher.source,
script: watcher.script,
immediate: watcher.immediate,
})
.then(async () => {
// setValue didn't update the list, so reloading explicitly
await studioPageWatchers.reload()
showWatcherDialog.value = false
})
}

const deletePageWatcher = async (watcher: StudioPageWatcher) => {
const confirmed = await confirm(`Are you sure you want to delete the watcher for ${watcher.source}?`)
if (confirmed) {
studioPageWatchers.delete
.submit(watcher.name)
.then(() => {
toast.success(`Watcher for ${watcher.source} deleted successfully`)
})
.catch(() => {
toast.error(`Failed to delete watcher for ${watcher.source}`)
})
}
}
</script>
1 change: 1 addition & 0 deletions frontend/src/components/ComponentEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ watchEffect(() => {

canvasStore.activeCanvas?.activeBreakpoint
canvasStore.dropTarget.placeholder
canvasStore.dropTarget.index
canvasStore.activeCanvas?.canvasProps.breakpoints.map((breakpoint) => breakpoint.visible)

nextTick(() => {
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/ComponentEvents.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -138,7 +137,6 @@ const eventOptions = computed(() => {
"keypress",
]
})
const { variableOptions } = useVariables(store.variables)

const doctypeFields = ref<{ label: string; value: string }[]>([])
watch(
Expand Down Expand Up @@ -297,7 +295,7 @@ const actions: ActionConfigurations = {
label: "Variable",
fieldname: "value",
fieldtype: "select",
options: variableOptions.value,
options: store.variableOptions,
},
],
rows: newEvent.value.fields,
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/ComponentProps.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
/>
<Autocomplete
v-if="propName === 'modelValue'"
:options="variableOptions"
:options="store.variableOptions"
placeholder="Select variable"
@update:modelValue="(variable: SelectOption) => bindVariable(propName, variable.value)"
>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/DataPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@
:name="variable_name"
class="-ml-[0.9rem] overflow-hidden"
/>
<div v-else class="flex flex-row justify-between">
<div class="text-sm font-semibold text-pink-700">{{ variable_name }}</div>
<div v-else class="flex flex-row justify-between font-mono text-xs">
<div class="font-semibold text-pink-700">{{ variable_name }}</div>
<template v-if="value !== ''">
<div class="text-xs text-gray-600">&nbsp;=&nbsp;</div>
<div class="text-sm text-violet-700">{{ value }}</div>
<div class="text-gray-600">&nbsp;=&nbsp;</div>
<div class="text-violet-700">{{ value }}</div>
</template>
</div>
<div
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/components/Input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,18 @@ import { useVModel } from "@vueuse/core"
import { useAttrs } from "vue"
import CrossIcon from "./Icons/Cross.vue"

const props = defineProps(["modelValue", "type", "hideClearButton"])
const props = withDefaults(
defineProps<{
modelValue?: string | number | boolean | null
type?: string
hideClearButton?: boolean
}>(),
{
type: "text",
modelValue: "",
},
)

const emit = defineEmits(["update:modelValue", "input"])
const data = useVModel(props, "modelValue", emit)

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/StudioCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
</div>
</div>
<div
class="canvas relative flex h-full rounded-md bg-white shadow-2xl contain-layout"
class="canvas relative flex h-full bg-white shadow-2xl contain-layout"
:style="{
...canvasStyles,
background: canvasProps.background,
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/StudioLeftPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
</div>

<DataPanel v-show="activeTab === 'Data'" />

<div v-show="activeTab === 'Code'">
<CodePanel class="p-4" v-if="store.activePage" :page="store.activePage" :key="store.selectedPage" />
</div>
</div>
</transition>
</div>
Expand All @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/data/studioWatchers.ts
Original file line number Diff line number Diff line change
@@ -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 }
1 change: 1 addition & 0 deletions frontend/src/pages/AppContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
Loading