From e45172b5546244d78d9e1f00e853a3c8c001fd38 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 21 Jul 2026 19:22:54 +0530 Subject: [PATCH 01/11] feat: provide access to slot props --- frontend/components.d.ts | 2 +- frontend/src/components/AppComponent.vue | 26 +++++----- .../src/components/AppLayout/Repeater.vue | 6 +-- .../AppLayout/RepeaterContextProvider.vue | 21 -------- .../src/components/DynamicValueSelector.vue | 29 +++++++---- frontend/src/components/SlotScopeProvider.vue | 19 +++++++ frontend/src/components/StudioComponent.vue | 32 ++++++------ frontend/src/stores/codeStore.ts | 12 ++--- frontend/src/types/index.ts | 8 +-- frontend/src/utils/block.ts | 50 ++++++++----------- frontend/src/utils/serializer.ts | 2 +- 11 files changed, 102 insertions(+), 105 deletions(-) delete mode 100644 frontend/src/components/AppLayout/RepeaterContextProvider.vue create mode 100644 frontend/src/components/SlotScopeProvider.vue diff --git a/frontend/components.d.ts b/frontend/components.d.ts index f6d3c2b80..36cdb7d61 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -92,7 +92,6 @@ declare module 'vue' { ProxyDialog: typeof import('./src/components/ProxyComponents/ProxyDialog.vue')['default'] PublishButton: typeof import('./src/components/PublishButton.vue')['default'] Repeater: typeof import('./src/components/AppLayout/Repeater.vue')['default'] - RepeaterContextProvider: typeof import('./src/components/AppLayout/RepeaterContextProvider.vue')['default'] ResourceDialog: typeof import('./src/components/ResourceDialog.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] @@ -101,6 +100,7 @@ declare module 'vue' { SectionContainer: typeof import('./src/components/SectionContainer.vue')['default'] SettingItem: typeof import('./src/components/SettingItem.vue')['default'] SlotIcon: typeof import('./src/components/Icons/SlotIcon.vue')['default'] + SlotScopeProvider: typeof import('./src/components/SlotScopeProvider.vue')['default'] SplitView: typeof import('./src/components/AppLayout/SplitView.vue')['default'] StudioCanvas: typeof import('./src/components/StudioCanvas.vue')['default'] StudioComponent: typeof import('./src/components/StudioComponent.vue')['default'] diff --git a/frontend/src/components/AppComponent.vue b/frontend/src/components/AppComponent.vue index 01b4da194..84b80063d 100644 --- a/frontend/src/components/AppComponent.vue +++ b/frontend/src/components/AppComponent.vue @@ -22,12 +22,13 @@ v-slot:[slotName]="slotProps" > diff --git a/frontend/src/components/DynamicValueSelector.vue b/frontend/src/components/DynamicValueSelector.vue index bba6eb494..a75dc4623 100644 --- a/frontend/src/components/DynamicValueSelector.vue +++ b/frontend/src/components/DynamicValueSelector.vue @@ -53,6 +53,7 @@ import useComponentEditorStore from "@/stores/componentEditorStore" import Block from "@/utils/block" import type { VariableOption } from "@/types/Studio/StudioPageVariable" import type { ComponentInput } from "@/types/Studio/StudioComponent" +import type { SlotScope } from "@/types" import { isObjectEmpty } from "@/utils/helpers" import { getBindingType } from "@/utils/parseCode" import useCodeStore from "@/stores/codeStore" @@ -141,20 +142,28 @@ const dynamicValueOptions = computed(() => { } } - // Repeater Data Item group - const repeaterContext = props.block?.repeaterDataItem - if (!isObjectEmpty(repeaterContext)) { - const repeaterOptions = Object.keys(repeaterContext!).map((key) => ({ - value: `dataItem.${key}`, - label: `dataItem.${key}`, - type: typeof repeaterContext![key], - })) + // Scoped slot props exposed by the enclosing component (Repeater's dataItem, List's item, ...) + const slotScopeOptions = getSlotScopeOptions(props.block?.slotScope) + if (slotScopeOptions.length) { groups.push({ - group: "Repeater", - items: repeaterOptions, + group: "Slot Scope", + items: slotScopeOptions, }) } return groups }) + +function getSlotScopeOptions(slotScope?: SlotScope | null): VariableOption[] { + return Object.entries(slotScope || {}).flatMap(([name, value]) => { + if (value && typeof value === "object" && !Array.isArray(value)) { + return Object.keys(value).map((key) => ({ + value: `${name}.${key}`, + label: `${name}.${key}`, + type: typeof value[key], + })) + } + return [{ value: name, label: name, type: typeof value }] + }) +} diff --git a/frontend/src/components/SlotScopeProvider.vue b/frontend/src/components/SlotScopeProvider.vue new file mode 100644 index 000000000..1e394c4a2 --- /dev/null +++ b/frontend/src/components/SlotScopeProvider.vue @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/components/StudioComponent.vue b/frontend/src/components/StudioComponent.vue index da7ce2b9b..aa399b1ee 100644 --- a/frontend/src/components/StudioComponent.vue +++ b/frontend/src/components/StudioComponent.vue @@ -38,16 +38,17 @@ v-slot:[slotName]="slotProps" > @@ -62,7 +54,7 @@ import Block from "@/utils/block" import { computed, onMounted, ref, useAttrs, inject, type ComputedRef, h } from "vue" import type { ComponentPublicInstance } from "vue" import { createResource } from "frappe-ui" -import { getComponentRoot, isHTML, isObjectEmpty } from "@/utils/helpers" +import { getComponentRoot, isObjectEmpty } from "@/utils/helpers" import { useScreenSize } from "@/utils/useScreenSize" import { isDynamicValue } from "@/utils/code" import { resolveEventListener } from "@/utils/eventModifiers" diff --git a/frontend/src/components/ComponentContextMenu.vue b/frontend/src/components/ComponentContextMenu.vue index ff73cfad4..dd698a27b 100644 --- a/frontend/src/components/ComponentContextMenu.vue +++ b/frontend/src/components/ComponentContextMenu.vue @@ -17,17 +17,14 @@ import { ref, Ref } from "vue" import { vOnClickOutside } from "@vueuse/components" import ContextMenu from "@/components/ContextMenu.vue" import Block from "@/utils/block" -import useStudioStore from "@/stores/studioStore" import useCanvasStore from "@/stores/canvasStore" import useComponentEditorStore from "@/stores/componentEditorStore" import type { ContextMenuOption } from "@/types" -import { isObjectEmpty } from "@/utils/helpers" import { getBlockCopy, getComponentBlock } from "@/utils/serializer" import getBlockTemplate from "@/utils/blockTemplate" import FormDialog from "@/components/FormDialog.vue" import { toast } from "frappe-ui" -const store = useStudioStore() const canvasStore = useCanvasStore() const contextMenuVisible = ref(false) @@ -129,15 +126,6 @@ const contextMenuOptions: ContextMenuOption[] = [ return !block.value.isRoot() && Boolean(block.value.getParentBlock()) }, }, - { - label: "Edit Slot", - action: () => { - store.showSlotEditorDialog = true - }, - condition: () => - !isObjectEmpty(block.value.componentSlots) && - block.value.isSlotEditable(canvasStore.activeCanvas?.selectedSlot), - }, { label: "Save as Component", action: () => { diff --git a/frontend/src/components/ComponentEditor.vue b/frontend/src/components/ComponentEditor.vue index 91fc94b75..874b5c82f 100644 --- a/frontend/src/components/ComponentEditor.vue +++ b/frontend/src/components/ComponentEditor.vue @@ -60,39 +60,13 @@ - - - -