diff --git a/apps/studio/electron/main/run/helpers.ts b/apps/studio/electron/main/run/helpers.ts index 935ad1f44..28166d9e8 100644 --- a/apps/studio/electron/main/run/helpers.ts +++ b/apps/studio/electron/main/run/helpers.ts @@ -1,6 +1,6 @@ import { type GeneratorOptions } from '@babel/generator'; import * as t from '@babel/types'; -import type { TemplateNode, TemplateTag } from '@onlook/models/element'; +import type { DynamicType, TemplateNode, TemplateTag } from '@onlook/models/element'; import * as fs from 'fs'; import { customAlphabet } from 'nanoid/non-secure'; import * as nodePath from 'path'; @@ -62,6 +62,7 @@ export function getTemplateNode( path: any, filename: string, componentStack: string[], + dynamicType?: DynamicType, ): TemplateNode { const startTag: TemplateTag = getTemplateTag(path.node.openingElement); const endTag: TemplateTag | null = path.node.closingElement @@ -73,6 +74,7 @@ export function getTemplateNode( startTag, endTag, component, + dynamicType, }; return domNode; } diff --git a/apps/studio/electron/main/run/setup.ts b/apps/studio/electron/main/run/setup.ts index 21dda6152..0174959d1 100644 --- a/apps/studio/electron/main/run/setup.ts +++ b/apps/studio/electron/main/run/setup.ts @@ -1,7 +1,7 @@ import traverse, { NodePath } from '@babel/traverse'; import * as t from '@babel/types'; import { EditorAttributes } from '@onlook/models/constants'; -import type { TemplateNode } from '@onlook/models/element'; +import type { DynamicType, TemplateNode } from '@onlook/models/element'; import { generateCode } from '../code/diff/helpers'; import { formatContent, readFile } from '../code/files'; import { parseJsxFile } from '../code/helpers'; @@ -67,6 +67,8 @@ export function createMappingFromContent(content: string, filename: string) { function createMapping(ast: t.File, filename: string): Record | null { const mapping: Record = {}; const componentStack: string[] = []; + const dynamicTypeStack: DynamicType[] = []; + const isFirstLayer: boolean[] = []; traverse(ast, { FunctionDeclaration: { @@ -93,6 +95,28 @@ function createMapping(ast: t.File, filename: string): Record 0 && isInFirstLayer + ? dynamicTypeStack[dynamicTypeStack.length - 1] + : undefined; + const templateNode = getTemplateNode( + path, + filename, + componentStack, + currentDynamicType, + ); + mapping[elementId] = templateNode; } + + // After processing the first JSX element in a map, mark that we're no longer in first layer + if (isFirstLayer[isFirstLayer.length - 1]) { + isFirstLayer[isFirstLayer.length - 1] = false; + } }, }); return mapping; diff --git a/apps/studio/electron/preload/webview/api.ts b/apps/studio/electron/preload/webview/api.ts index 112cb1c97..9f50b67cf 100644 --- a/apps/studio/electron/preload/webview/api.ts +++ b/apps/studio/electron/preload/webview/api.ts @@ -1,7 +1,12 @@ import { contextBridge } from 'electron'; import { processDom } from './dom'; import { getDomElementByDomId, getElementAtLoc, updateElementInstance } from './elements'; -import { getActionElementByDomId, getActionLocation } from './elements/dom/helpers'; +import { + getActionElementByDomId, + getActionLocation, + setDynamicElementType, + getDynamicElementType, +} from './elements/dom/helpers'; import { getInsertLocation } from './elements/dom/insert'; import { getRemoveActionFromDomId } from './elements/dom/remove'; import { getElementIndex } from './elements/move'; @@ -22,6 +27,8 @@ export function setApi() { // Elements getElementAtLoc, getDomElementByDomId, + setDynamicElementType, + getDynamicElementType, // Actions getActionLocation, diff --git a/apps/studio/electron/preload/webview/elements/dom/helpers.ts b/apps/studio/electron/preload/webview/elements/dom/helpers.ts index 9378b175d..e132b0562 100644 --- a/apps/studio/electron/preload/webview/elements/dom/helpers.ts +++ b/apps/studio/electron/preload/webview/elements/dom/helpers.ts @@ -3,6 +3,8 @@ import { getOrAssignDomId } from '../../ids'; import { getImmediateTextContent } from '../helpers'; import { elementFromDomId } from '/common/helpers'; import { getInstanceId, getOid } from '/common/helpers/ids'; +import { EditorAttributes } from '@onlook/models/constants'; +import type { DynamicType } from '@onlook/models/element'; export function getActionElementByDomId(domId: string): ActionElement | null { const el = elementFromDomId(domId); @@ -77,3 +79,23 @@ export function getActionLocation(domId: string): ActionLocation | null { originalIndex: index, }; } + +export function getDynamicElementType(domId: string): DynamicType | null { + const el = document.querySelector( + `[${EditorAttributes.DATA_ONLOOK_DOM_ID}="${domId}"]`, + ) as HTMLElement | null; + + if (!el) { + console.warn('No element found', { domId }); + return null; + } + + return el.getAttribute(EditorAttributes.DATA_ONLOOK_DYNAMIC_TYPE) as DynamicType; +} + +export function setDynamicElementType(domId: string, dynamicType: string) { + const el = document.querySelector(`[${EditorAttributes.DATA_ONLOOK_DOM_ID}="${domId}"]`); + if (el) { + el.setAttribute(EditorAttributes.DATA_ONLOOK_DYNAMIC_TYPE, dynamicType); + } +} diff --git a/apps/studio/src/lib/editor/engine/ast/index.ts b/apps/studio/src/lib/editor/engine/ast/index.ts index 0e2ff9638..5b651f2bc 100644 --- a/apps/studio/src/lib/editor/engine/ast/index.ts +++ b/apps/studio/src/lib/editor/engine/ast/index.ts @@ -74,6 +74,16 @@ export class AstManager { return; } + if (templateNode.dynamicType) { + node.dynamicType = templateNode.dynamicType; + const webview = this.editorEngine.webviews.getWebview(webviewId); + if (webview) { + webview.executeJavaScript( + `window.api?.setDynamicElementType('${node.domId}', '${templateNode.dynamicType}')`, + ); + } + } + this.findNodeInstance(webviewId, node, node, templateNode); } diff --git a/apps/studio/src/lib/editor/engine/element/index.ts b/apps/studio/src/lib/editor/engine/element/index.ts index 40198bb8f..73ab3580b 100644 --- a/apps/studio/src/lib/editor/engine/element/index.ts +++ b/apps/studio/src/lib/editor/engine/element/index.ts @@ -3,6 +3,7 @@ import type { DomElement } from '@onlook/models/element'; import { debounce } from 'lodash'; import { makeAutoObservable } from 'mobx'; import type { EditorEngine } from '..'; +import { toast } from '@onlook/ui/use-toast'; export class ElementManager { private hoveredElement: DomElement | undefined; @@ -155,6 +156,20 @@ export class ElementManager { return; } + const dynamicElementType = await webview.executeJavaScript( + `window.api?.getDynamicElementType('${selectedEl.domId}')`, + ); + + if (dynamicElementType) { + toast({ + title: 'Invalid Action', + description: `This element is part of a react expression (${dynamicElementType}) and cannot be deleted`, + variant: 'destructive', + }); + + return; + } + const removeAction = (await webview.executeJavaScript( `window.api?.getRemoveActionFromDomId('${selectedEl.domId}', '${webviewId}')`, )) as RemoveElementAction | null; @@ -166,6 +181,7 @@ export class ElementManager { if (!codeBlock) { console.error('Code block not found'); } + this.editorEngine.action.run(removeAction); } } diff --git a/packages/models/src/constants/index.ts b/packages/models/src/constants/index.ts index 362659b0e..b43b9c620 100644 --- a/packages/models/src/constants/index.ts +++ b/packages/models/src/constants/index.ts @@ -21,6 +21,7 @@ export enum EditorAttributes { DATA_ONLOOK_DRAG_START_POSITION = 'data-onlook-drag-start-position', DATA_ONLOOK_NEW_INDEX = 'data-onlook-new-index', DATA_ONLOOK_EDITING_TEXT = 'data-onlook-editing-text', + DATA_ONLOOK_DYNAMIC_TYPE = 'data-onlook-dynamic-type', } export enum WebviewChannels { diff --git a/packages/models/src/element/layers.ts b/packages/models/src/element/layers.ts index 376a0f7ef..b9f4f37c8 100644 --- a/packages/models/src/element/layers.ts +++ b/packages/models/src/element/layers.ts @@ -1,5 +1,8 @@ import { z } from 'zod'; +export const DynamicTypeEnum = z.enum(['array', 'conditional', 'unknown']); +export type DynamicType = z.infer; + const LayerNodeSchema = z.object({ domId: z.string(), webviewId: z.string(), @@ -8,6 +11,7 @@ const LayerNodeSchema = z.object({ textContent: z.string(), tagName: z.string(), isVisible: z.boolean(), + dynamicType: DynamicTypeEnum.optional(), component: z.string().nullable(), children: z.array(z.string()).nullable(), parent: z.string().nullable(), diff --git a/packages/models/src/element/templateNode.ts b/packages/models/src/element/templateNode.ts index 4242a3b8d..7bcd8b1bb 100644 --- a/packages/models/src/element/templateNode.ts +++ b/packages/models/src/element/templateNode.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { DynamicTypeEnum } from './layers'; export const TemplateTagPositionSchema = z.object({ line: z.number(), @@ -15,6 +16,7 @@ export const TemplateNodeSchema = z.object({ startTag: TemplateTagSchema, endTag: TemplateTagSchema.nullable(), component: z.string().nullable(), + dynamicType: DynamicTypeEnum.nullable().optional(), }); export type TemplateNode = z.infer;