Skip to content

Commit 752a056

Browse files
authored
Merge pull request #31 from ruchamahabal/object-variables
feat: Ability to add Object type variables, two-way binding with nested object properties + misc fixes
2 parents a08289b + 592a2b7 commit 752a056

File tree

15 files changed

+235
-87
lines changed

15 files changed

+235
-87
lines changed

frontend/src/components/AppComponent.vue

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<component
33
ref="componentRef"
44
v-show="showComponent"
5-
:is="components.getComponent(block.componentName)"
5+
:is="block.componentName"
66
v-bind="componentProps"
77
v-model="boundValue"
88
:data-component-id="block.componentId"
@@ -31,11 +31,11 @@ import Block from "@/utils/block"
3131
import { computed, onMounted, ref, useAttrs } from "vue"
3232
import { useRouter, useRoute } from "vue-router"
3333
import { createResource } from "frappe-ui"
34-
import components from "@/data/components"
3534
import { getComponentRoot, isDynamicValue, getDynamicValue, isHTML, executeUserScript } from "@/utils/helpers"
3635
3736
import useAppStore from "@/stores/appStore"
3837
import { toast } from "vue-sonner"
38+
import { Field } from "@/types/ComponentEvent"
3939
4040
const props = defineProps<{
4141
block: Block
@@ -48,14 +48,15 @@ const store = useAppStore()
4848
const getComponentProps = () => {
4949
if (!props.block || props.block.isRoot()) return []
5050
51-
const componentProps = { ...props.block.componentProps }
51+
const propValues = { ...props.block.componentProps }
52+
delete propValues.modelValue
5253
53-
Object.entries(componentProps).forEach(([propName, config]) => {
54+
Object.entries(propValues).forEach(([propName, config]) => {
5455
if (isDynamicValue(config)) {
55-
componentProps[propName] = getDynamicValue(config, { ...store.resources, ...store.variables })
56+
propValues[propName] = getDynamicValue(config, { ...store.resources, ...store.variables })
5657
}
5758
})
58-
return componentProps
59+
return propValues
5960
}
6061
6162
const attrs = useAttrs()
@@ -75,24 +76,50 @@ const showComponent = computed(() => {
7576
return true
7677
})
7778
78-
// Computed property for v-model binding
79+
// modelValue binding
7980
const boundValue = computed({
8081
get() {
8182
const modelValue = props.block.componentProps.modelValue
8283
if (modelValue?.$type === "variable") {
83-
// Return the variable value from the store
84-
return store.variables[modelValue.name]
84+
// handle nested object properties
85+
const propertyPath = modelValue.name.split(".")
86+
let value = store.variables
87+
// return nested object property value
88+
for (const key of propertyPath) {
89+
if (value === undefined || value === null) return undefined
90+
value = value[key]
91+
}
92+
return value
93+
} else if (isDynamicValue(modelValue)) {
94+
return getDynamicValue(modelValue, { ...store.resources, ...store.variables })
8595
}
86-
// Return the plain value if not bound to a variable
8796
return modelValue
8897
},
8998
set(newValue) {
9099
const modelValue = props.block.componentProps.modelValue
91100
if (modelValue?.$type === "variable") {
92-
// Update the variable in the store
93-
store.variables[modelValue.name] = newValue
101+
// update the variable in the store
102+
const propertyPath = modelValue.name.split(".")
103+
if (propertyPath.length === 1) {
104+
// top level variable
105+
store.variables[modelValue.name] = newValue
106+
} else {
107+
// nested object properties
108+
const targetProperty = propertyPath.pop()!
109+
let obj = store.variables
110+
111+
// navigate to the parent object
112+
for (const key of propertyPath) {
113+
if (!obj[key] || typeof obj[key] !== "object") {
114+
obj[key] = {}
115+
}
116+
obj = obj[key]
117+
}
118+
// set the value on the parent object
119+
obj[targetProperty] = newValue
120+
}
94121
} else {
95-
// Update the prop directly if not bound to a variable
122+
// update the prop directly if not bound to a variable
96123
props.block.setProp("modelValue", newValue)
97124
}
98125
},
@@ -132,8 +159,8 @@ const componentEvents = computed(() => {
132159
}
133160
} else if (event.action === "Insert a Document") {
134161
return () => {
135-
const fields = {}
136-
event.fields.forEach((field) => {
162+
const fields: Record<string, any> = {}
163+
event.fields.forEach((field: Field) => {
137164
fields[field.field] = store.variables[field.value]
138165
})
139166
createResource({

frontend/src/components/CodeEditor.vue

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Save
1919
</Button>
2020
</div>
21+
<ErrorMessage class="text-xs leading-4" v-if="errorMessage" :message="errorMessage" />
2122
</div>
2223
</template>
2324

@@ -74,10 +75,7 @@ const props = defineProps({
7475
const emit = defineEmits(["save", "update:modelValue"])
7576
const editor = ref<HTMLElement | null>(null)
7677
let aceEditor = null as ace.Ace.Editor | null
77-
78-
onMounted(() => {
79-
setupEditor()
80-
})
78+
const errorMessage = ref("")
8179
8280
const setupEditor = () => {
8381
aceEditor = ace.edit(editor.value as HTMLElement)
@@ -112,8 +110,13 @@ const setupEditor = () => {
112110
}
113111
aceEditor.on("blur", () => {
114112
try {
113+
errorMessage.value = ""
115114
let value = aceEditor?.getValue() || ""
116-
if ((props.type === "JSON" || typeof props.modelValue === "object") && !value.startsWith("{{")) {
115+
if (
116+
value &&
117+
!value.startsWith("{{") &&
118+
(props.type === "JSON" || typeof props.modelValue === "object")
119+
) {
117120
value = jsonToJs(value)
118121
if (areObjectsEqual(value, props.modelValue)) return
119122
} else if (value === props.modelValue) {
@@ -125,6 +128,7 @@ const setupEditor = () => {
125128
}
126129
} catch (e) {
127130
console.error("Error while parsing JSON for editor", e)
131+
errorMessage.value = `Invalid object/JSON: ${e.message}`
128132
}
129133
})
130134
}
@@ -170,6 +174,10 @@ watch(
170174
},
171175
)
172176
177+
onMounted(() => {
178+
setupEditor()
179+
})
180+
173181
defineExpose({ resetEditor })
174182
</script>
175183
<style scoped>

frontend/src/components/ComponentEvents.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import EmptyState from "@/components/EmptyState.vue"
9292
9393
import { isObjectEmpty, confirm } from "@/utils/helpers"
9494
import { getComponentEvents } from "@/utils/components"
95+
import { useVariables } from "@/utils/useVariables"
9596
9697
import { SelectOption } from "@/types"
9798
import { Actions, ActionConfigurations, ComponentEvent } from "@/types/ComponentEvent"
@@ -137,6 +138,7 @@ const eventOptions = computed(() => {
137138
"keypress",
138139
]
139140
})
141+
const { variableOptions } = useVariables(store.variables)
140142
141143
const doctypeFields = ref<{ label: string; value: string }[]>([])
142144
watch(
@@ -295,7 +297,7 @@ const actions: ActionConfigurations = {
295297
label: "Variable",
296298
fieldname: "value",
297299
fieldtype: "select",
298-
options: Object.keys(store.variables),
300+
options: variableOptions.value,
299301
},
300302
],
301303
rows: newEvent.value.fields,
@@ -318,6 +320,7 @@ const actions: ActionConfigurations = {
318320
label: "Script",
319321
type: "JavaScript",
320322
modelValue: newEvent.value.script,
323+
showLineNumbers: true,
321324
}
322325
},
323326
events: {

frontend/src/components/ComponentProps.vue

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
/>
3030
<Autocomplete
3131
v-if="propName === 'modelValue'"
32-
:options="variables"
32+
:options="variableOptions"
3333
placeholder="Select variable"
3434
@update:modelValue="(variable: SelectOption) => bindVariable(propName, variable.value)"
3535
>
@@ -132,6 +132,7 @@ import useStudioStore from "@/stores/studioStore"
132132
import IconButton from "@/components/IconButton.vue"
133133
import CodeEditor from "@/components/CodeEditor.vue"
134134
import blockController from "@/utils/blockController"
135+
import { useVariables } from "@/utils/useVariables"
135136
136137
const props = defineProps<{
137138
block?: Block
@@ -193,7 +194,7 @@ const getSlotContent = (slot: Slot) => {
193194
}
194195
195196
// variable binding
196-
const variables = computed(() => Object.keys(store.variables))
197+
const { variableOptions } = useVariables(store.variables)
197198
const boundValue = computed({
198199
get() {
199200
const modelValue = props.block?.componentProps.modelValue
@@ -203,13 +204,6 @@ const boundValue = computed({
203204
return modelValue
204205
},
205206
set(newValue) {
206-
if (typeof newValue === "string" && newValue.startsWith("{{")) {
207-
const varName = newValue.replace(/[{} ]/g, "")
208-
if (store.variables.value[varName]) {
209-
bindVariable("modelValue", varName)
210-
return
211-
}
212-
}
213207
props.block?.setProp("modelValue", newValue)
214208
},
215209
})

frontend/src/components/DataPanel.vue

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<template>
22
<div class="flex flex-col gap-3 p-4">
33
<CollapsibleSection sectionName="Data Sources">
4-
<div class="flex flex-col gap-2" v-if="!isObjectEmpty(store.resources)">
4+
<div class="ml-3 flex flex-col gap-2" v-if="!isObjectEmpty(store.resources)">
55
<div
66
v-for="(resource, resource_name) in store.resources"
77
:key="resource_name"
88
class="group/resource flex flex-row justify-between"
99
>
10-
<ObjectBrowser :object="resource" :name="resource_name" class="overflow-hidden" />
10+
<ObjectBrowser :object="resource" :name="resource_name" class="-ml-[0.9rem] overflow-hidden" />
1111
<div
1212
class="invisible -mt-1 ml-auto self-start text-gray-600 group-hover/resource:visible has-[.active-item]:visible"
1313
>
@@ -40,7 +40,7 @@
4040

4141
<!-- Variables -->
4242
<CollapsibleSection sectionName="Variables">
43-
<div class="flex flex-col gap-1" v-if="!isObjectEmpty(store.variables)">
43+
<div class="ml-3 flex flex-col gap-1" v-if="!isObjectEmpty(store.variables)">
4444
<div
4545
v-for="(value, variable_name) in store.variables"
4646
:key="variable_name"
@@ -50,7 +50,7 @@
5050
v-if="typeof value === 'object'"
5151
:object="value"
5252
:name="variable_name"
53-
class="overflow-hidden"
53+
class="-ml-[0.9rem] overflow-hidden"
5454
/>
5555
<div v-else class="flex flex-row justify-between">
5656
<div class="text-sm font-semibold text-pink-700">{{ variable_name }}</div>
@@ -106,13 +106,33 @@
106106
<FormControl
107107
label="Variable Type"
108108
type="select"
109-
:options="['String', 'Number', 'Boolean']"
109+
:options="['String', 'Number', 'Boolean', 'Object']"
110110
v-model="variableRef.variable_type"
111111
:required="true"
112112
default="String"
113113
@change="() => setInitialValue()"
114114
/>
115-
<FormControl label="Initial Value" v-model="variableRef.initial_value" autocomplete="off" />
115+
<CodeEditor
116+
v-if="variableRef.variable_type === 'Object'"
117+
label="Initial Value"
118+
type="JavaScript"
119+
height="250px"
120+
:showLineNumbers="true"
121+
v-model="variableRef.initial_value"
122+
/>
123+
<FormControl
124+
v-else-if="variableRef.variable_type === 'Number'"
125+
label="Initial Value"
126+
type="number"
127+
:modelValue="variableRef.initial_value"
128+
@update:modelValue="variableRef.initial_value = Number($event)"
129+
/>
130+
<FormControl
131+
v-else
132+
label="Initial Value"
133+
v-model="variableRef.initial_value"
134+
autocomplete="off"
135+
/>
116136
</div>
117137
</template>
118138
<template #actions>
@@ -144,6 +164,7 @@ import CollapsibleSection from "@/components/CollapsibleSection.vue"
144164
import ObjectBrowser from "@/components/ObjectBrowser.vue"
145165
import EmptyState from "@/components/EmptyState.vue"
146166
import ResourceDialog from "@/components/ResourceDialog.vue"
167+
import CodeEditor from "@/components/CodeEditor.vue"
147168
148169
import { isObjectEmpty, getAutocompleteValues, confirm, copyToClipboard } from "@/utils/helpers"
149170
import { studioResources, studioPageResources } from "@/data/studioResources"
@@ -295,34 +316,63 @@ const setInitialValue = () => {
295316
variableRef.value.initial_value = 0
296317
} else if (variableRef.value.variable_type === "Boolean") {
297318
variableRef.value.initial_value = false
319+
} else if (variableRef.value.variable_type === "Object") {
320+
variableRef.value.initial_value = {}
298321
}
299322
}
300323
324+
const getInitialValue = (variable: Variable) => {
325+
if (variable.variable_type === "Object" && typeof variable.initial_value !== "string") {
326+
try {
327+
return JSON.stringify(variable.initial_value)
328+
} catch (error) {
329+
toast.error("Invalid Object")
330+
throw new Error("Invalid Object")
331+
}
332+
} else if (variable.variable_type === "String" && !variable.initial_value) {
333+
return JSON.stringify("")
334+
} else if (variable.variable_type === "Boolean" && typeof variable.initial_value === "string") {
335+
// return string as is - to avoid saving false as "false" in the backend field
336+
return variable.initial_value
337+
}
338+
return JSON.stringify(variable.initial_value)
339+
}
340+
301341
const addVariable = (variable: Variable) => {
302-
studioVariables.insert
303-
.submit({
342+
const initial_value = getInitialValue(variable)
343+
studioVariables.insert.submit(
344+
{
304345
variable_name: variable.variable_name,
305346
variable_type: variable.variable_type,
306-
initial_value: variable.initial_value?.toString(),
347+
initial_value: initial_value,
307348
parent: store.activePage?.name,
308349
parenttype: "Studio Page",
309350
parentfield: "variables",
310-
})
311-
.then(async () => {
312-
if (store.activePage) {
313-
await store.setPageVariables(store.activePage)
314-
}
315-
showVariableDialog.value = false
316-
})
351+
},
352+
{
353+
async onSuccess() {
354+
if (store.activePage) {
355+
await store.setPageVariables(store.activePage)
356+
}
357+
showVariableDialog.value = false
358+
},
359+
onError(error: any) {
360+
toast.error("Failed to add variable", {
361+
description: error.messages.join(", "),
362+
})
363+
},
364+
},
365+
)
317366
}
318367
319368
const editVariable = (variable: Variable) => {
369+
const initial_value = getInitialValue(variable)
320370
studioVariables.setValue
321371
.submit({
322372
name: variable.name,
323373
variable_name: variable.variable_name,
324374
variable_type: variable.variable_type,
325-
initial_value: variable.initial_value?.toString(),
375+
initial_value: initial_value,
326376
})
327377
.then(async () => {
328378
if (store.activePage) {

0 commit comments

Comments
 (0)