Skip to content
Draft
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
54 changes: 54 additions & 0 deletions builder/builder/doctype/builder_page/builder_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,9 @@ def get_block_html(blocks):
font_map = {}

def get_html(blocks, soup):
map_of_inherited_props = {} # prop_name -> array<values>
html = ""
all_block_scripts = []

def get_tag(block, soup, data_key=None):
block = extend_with_component(block)
Expand Down Expand Up @@ -566,6 +568,35 @@ def get_tag(block, soup, data_key=None):
set_fonts_from_html(inner_soup, font_map)
tag.append(inner_soup)

props_obj = {}
props_with_successors = []
if block.get("props"):
props = []
for key, value in block.get("props", {}).items():

prop_value = value["value"]
prop_type = value["type"]
interpreted_value = ""

if prop_value == "" or prop_value is None:
prop_value = "undefined"

if prop_type == "dynamic":
interpreted_value = f"{{{{ {data_key}.{prop_value} }}}}" if data_key else f"{{{{ {prop_value} }}}}"
elif prop_type == "static":
interpreted_value = f"{prop_value}"
elif prop_type == "inherited":
values = map_of_inherited_props.get(prop_value, [])
interpreted_value = values[0] if values else "undefined"

props.append(f"{key}: {interpreted_value}")

if value.get('usedByCount', 0) > 0:
props_with_successors.append(key)
map_of_inherited_props.setdefault(key, []).append(interpreted_value)

props_obj = f"{{ {', '.join(props)} }}"

if block.get("isRepeaterBlock") and block.get("children") and block.get("dataKey"):
_key = block.get("dataKey").get("key")
if data_key:
Expand All @@ -589,6 +620,21 @@ def get_tag(block, soup, data_key=None):
if element == "body":
tag.append("{% include 'templates/generators/webpage_scripts.html' %}")

for props in props_with_successors:
map_of_inherited_props[props].pop()

if block.get("blockScript"):
block_unique_id = f"{block.get('blockId')}-{frappe.generate_hash(length=3)}"
script_content = f"(function (props){{ {block.get('blockScript')} }}).call(document.querySelector('[data-block-id=\"{block_unique_id}\"]'), {props_obj or '{}'});"
print("Script content: ", script_content)
all_block_scripts.append(script_content)
tag.attrs["data-block-id"] = block_unique_id
if block.get("blockId") == "root":
for script in all_block_scripts:
script_tag = soup.new_tag("script")
script_tag.string = script
tag.append(script_tag)

return tag

for block in blocks:
Expand Down Expand Up @@ -699,6 +745,14 @@ def extend_block(block, overridden_block):
block["rawStyles"].update(overridden_block.get("rawStyles", {}))

block["classes"].extend(overridden_block["classes"])

if not block.get("props"):
block["props"] = {}
block["props"].update(overridden_block.get("props", {}))

if overridden_block.get("blockScript"):
block["blockScript"] = overridden_block.get("blockScript")

dataKey = overridden_block.get("dataKey", {})
if not block.get("dataKey"):
block["dataKey"] = {}
Expand Down
1 change: 1 addition & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ declare module 'vue' {
Play: typeof import('./src/components/Icons/Play.vue')['default']
Plus: typeof import('./src/components/Icons/Plus.vue')['default']
PropertyControl: typeof import('./src/components/Controls/PropertyControl.vue')['default']
PropsEditor: typeof import('./src/components/PropsEditor.vue')['default']
PublishButton: typeof import('./src/components/PublishButton.vue')['default']
RangeInput: typeof import('./src/components/Controls/RangeInput.vue')['default']
Redirect: typeof import('./src/components/Icons/Redirect.vue')['default']
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class Block implements BlockOptions {
parentBlock: Block | null;
activeState?: string | null = null;
dynamicValues: Array<BlockDataKey>;
blockScript?: string;
props?: BlockProps;
// @ts-expect-error
referenceComponent: Block | null;
customAttributes: BlockAttributeMap;
Expand Down Expand Up @@ -97,6 +99,8 @@ class Block implements BlockOptions {
this.tabletStyles = reactive(options.tabletStyles || {});
this.attributes = reactive(options.attributes || {});
this.dynamicValues = reactive(options.dynamicValues || []);
this.blockScript = options.blockScript || "";
this.props = reactive(options.props || {});

this.blockName = options.blockName;
delete this.attributes.style;
Expand Down Expand Up @@ -889,6 +893,30 @@ class Block implements BlockOptions {
isInsideRepeater(): boolean {
return Boolean(this.getRepeaterParent());
}
getBlockScript(): string {
let blockScript = "";
if (this.isExtendedFromComponent() && !this.blockScript) {
blockScript = this.referenceComponent?.getBlockScript() || "";
} else {
blockScript = this.blockScript || "";
}
return blockScript;
}
setBlockScript(script: string) {
this.blockScript = script;
}
getBlockProps(): BlockProps {
let blockProps = {};
if (this.isExtendedFromComponent() && !Object.keys(this.props || {}).length) {
blockProps = this.referenceComponent?.getBlockProps() || {};
} else {
blockProps = this.props || {};
}
return blockProps;
}
setBlockProps(props: BlockProps) {
this.props = props;
}
}

function extendWithComponent(
Expand Down Expand Up @@ -993,6 +1021,8 @@ function resetBlock(
block.customAttributes = {};
block.classes = [];
block.dataKey = null;
block.props = {};
block.blockScript = "";
}

if (resetChildren) {
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/builder.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ declare interface BlockStyleMap {
[key: styleProperty]: StyleValue;
}

declare type BlockProps = Record<
string,
{
type: "dynamic" | "static" | "inherited";
value: string?;
usedByCount?: number;
}
>;

declare interface BlockAttributeMap {
[key: string]: string | number | null | undefined;
}
Expand Down
51 changes: 50 additions & 1 deletion frontend/src/components/BlockLayers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
:group="{ name: 'block-tree' }"
item-key="blockId"
@add="updateParent"
@end="handleDragEnd"
:move="handleDragMove"
:disabled="disableDraggable">
<template #item="{ element }">
<div
Expand Down Expand Up @@ -94,10 +96,11 @@ import type Block from "@/block";
import useBuilderStore from "@/stores/builderStore";
import useCanvasStore from "@/stores/canvasStore";
import { FeatherIcon } from "frappe-ui";
import { ref, watch } from "vue";
import { nextTick, ref, watch } from "vue";
import draggable from "vuedraggable";
import BlockLayers from "./BlockLayers.vue";
import BlocksIcon from "./Icons/Blocks.vue";
import blockController from "@/utils/blockController";

type LayerInstance = InstanceType<typeof BlockLayers>;

Expand Down Expand Up @@ -228,6 +231,52 @@ const selectBlock = (block: Block, event: MouseEvent) => {
canvasStore.selectBlock(block, event, false, true);
};

const currentlyDraggingBlock = ref<Block | null>(null);

const handleDragMove = (event: any) => {
const draggedBlock: Block = event.draggedContext.element;
if (!currentlyDraggingBlock.value) {
currentlyDraggingBlock.value = draggedBlock;

const canvasStore = useCanvasStore();
const pauseId = canvasStore.activeCanvas?.history?.pause();

const listOfInheritedProps = [];
for (const propKey in draggedBlock.props) {
if (draggedBlock.props[propKey].type === "inherited" && draggedBlock.props[propKey].value) {
listOfInheritedProps.push(draggedBlock.props[propKey].value);
}
}
blockController.updateBlockPropsDependencyForAncestor(listOfInheritedProps, "remove", draggedBlock);

nextTick(() => {
pauseId && canvasStore.activeCanvas?.history?.resume(pauseId);
});
}
return true; // Allow the move
};

const handleDragEnd = async () => {
await nextTick();
if (currentlyDraggingBlock.value) {
const listOfInheritedProps = [];
for (const propKey in currentlyDraggingBlock.value.props) {
if (
currentlyDraggingBlock.value.props[propKey].type === "inherited" &&
currentlyDraggingBlock.value.props[propKey].value
) {
listOfInheritedProps.push(currentlyDraggingBlock.value.props[propKey].value);
}
}
blockController.updateBlockPropsDependencyForAncestor(
listOfInheritedProps,
"add",
currentlyDraggingBlock.value,
);
currentlyDraggingBlock.value = null;
}
};

defineExpose({
toggleExpanded,
isExpandedInTree,
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/BlockProperties.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import styleSection from "@/components/BlockPropertySections/StyleSection";
import transitionSection from "@/components/BlockPropertySections/TransitionSection";
import typographySection from "@/components/BlockPropertySections/TypographySection";
import videoOptionsSection from "@/components/BlockPropertySections/VideoOptionsSection";
import blockScriptSection from "@/components/BlockPropertySections/ScriptEditor";
import blockPropsSection from "@/components/BlockPropertySections/PropsSection";
import useBuilderStore from "@/stores/builderStore";
import blockController from "@/utils/blockController";
import { toValue } from "@vueuse/core";
Expand Down Expand Up @@ -119,5 +121,7 @@ const sections = [
dataKeySection,
customAttributesSection,
rawStyleSection,
blockScriptSection,
blockPropsSection,
] as PropertySection[];
</script>
39 changes: 39 additions & 0 deletions frontend/src/components/BlockPropertySections/PropsSection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import PropsEditor from "../PropsEditor.vue";
import blockController from "@/utils/blockController";
import { computed } from "vue";

const propsSection = [
{
component: PropsEditor,
getProps: () => {
return {
obj: blockController.getBlockProps(),
description: `
<b>Note:</b>
<br />
<br />
• A block can have multiple props
<br />
• Props can be static values, dynamic values (from data script), or inherited from ancestor blocks
`,
};
},
searchKeyWords: "Props, Interface Props, Properties, Block Props, Block Properties",
events: {
"update:obj": (obj: BlockProps) => blockController.setBlockProps(obj), // TODO: race condition?
"update:ancestorUpdateDependency": (
propKey: string,
action: "add" | "remove", // TODO: better name?
) => blockController.updateBlockPropsDependencyForAncestor(propKey, action),
},
},
];

export default {
name: "Props",
properties: propsSection,
collapsed: computed(() => {
return Object.keys(blockController.getBlockProps()).length === 0;
}),
condition: () => !blockController.multipleBlocksSelected(),
};
40 changes: 40 additions & 0 deletions frontend/src/components/BlockPropertySections/ScriptEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { computed } from "vue";
import CodeEditor from "../Controls/CodeEditor.vue";
import blockController from "@/utils/blockController";
import useCanvasStore from "@/stores/canvasStore";

const blockScriptProperties = [
{
component: CodeEditor,
getProps: () => {
return {
modelValue: blockController.getBlockScript(),
getModelValue: () => blockController.getBlockScript() || "",
type: "JavaScript",
readonly: false,
height: "200px",
showLineNumbers: true,
autofocus: true,
actionButton: {
label: "Expand",
icon: "maximize-2",
handler: () => {
useCanvasStore().editBlockScript(blockController.getSelectedBlocks()[0]);
},
},
};
},
searchKeyWords: "Block Script, Script, JS, JavaScript, Custom Script",
events: {
save: (script: string) => blockController.setBlockScript(script),
"update:modelValue": (script: string) => blockController.setBlockScript(script),
},
},
];

export default {
name: "Block Script",
properties: blockScriptProperties,
collapsed: false,
condition: () => !blockController.multipleBlocksSelected(),
};
1 change: 1 addition & 0 deletions frontend/src/components/Controls/Input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<button
class="cursor-pointer text-ink-gray-4 hover:text-ink-gray-5"
tabindex="-1"
:disabled="Boolean(attrs.disabled)"
@click="clearValue">
<CrossIcon />
</button>
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/components/Modals/NewComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,19 @@ const componentName = ref("");
const isGlobalComponent = ref(0);

const createComponentHandler = async (context: { close: () => void }) => {
const blockCopy = getBlockCopy(props.block, true);
// clean block with inherited or dynamic props
const cleanBlock = props.block;
if (props.block.props) {
const cleanProps = cleanBlock.props || {};
Object.entries(props.block.props).forEach(([key, value]) => {
if (value.type === 'inherited' || value.type === 'dynamic') {
cleanProps[key] = {...value, type: 'static' , value: null};
}
});
cleanBlock.props = cleanProps;
}

const blockCopy = getBlockCopy(cleanBlock, true);
blockCopy.removeStyle("left");
blockCopy.removeStyle("top");
blockCopy.removeStyle("position");
Expand Down
Loading
Loading