Skip to content

Commit

Permalink
Prevent deleting core elements (#917)
Browse files Browse the repository at this point in the history
  • Loading branch information
iNerdStack authored Dec 21, 2024
1 parent caa656e commit 84715f5
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 23 deletions.
23 changes: 22 additions & 1 deletion apps/studio/electron/main/run/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { type GeneratorOptions } from '@babel/generator';
import * as t from '@babel/types';
import type { DynamicType, TemplateNode, TemplateTag } from '@onlook/models/element';
import type {
CoreElementType,
DynamicType,
TemplateNode,
TemplateTag,
} from '@onlook/models/element';
import * as fs from 'fs';
import { customAlphabet } from 'nanoid/non-secure';
import * as nodePath from 'path';
Expand Down Expand Up @@ -64,6 +69,7 @@ export function getTemplateNode(
filename: string,
componentStack: string[],
dynamicType?: DynamicType,
coreElementType?: CoreElementType,
): TemplateNode {
const startTag: TemplateTag = getTemplateTag(path.node.openingElement);
const endTag: TemplateTag | null = path.node.closingElement
Expand All @@ -76,6 +82,7 @@ export function getTemplateNode(
endTag,
component,
dynamicType,
coreElementType,
};
return domNode;
}
Expand Down Expand Up @@ -119,3 +126,17 @@ export function getDynamicTypeInfo(path: NodePath<t.JSXElement>): DynamicType |

return dynamicType;
}

export function getCoreElementInfo(path: NodePath<t.JSXElement>): CoreElementType | undefined {
const parent = path.parent;

const isComponentRoot = t.isReturnStatement(parent) || t.isArrowFunctionExpression(parent);

const isBodyTag =
t.isJSXIdentifier(path.node.openingElement.name) &&
path.node.openingElement.name.name === 'body';

const coreElementType = isComponentRoot ? 'component-root' : isBodyTag ? 'body-tag' : undefined;

return coreElementType;
}
10 changes: 9 additions & 1 deletion apps/studio/electron/main/run/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isReactFragment,
getDynamicTypeInfo,
isNodeElementArray,
getCoreElementInfo,
} from './helpers';

export async function getFileWithIds(filePath: string): Promise<string | null> {
Expand Down Expand Up @@ -147,8 +148,15 @@ function createMapping(ast: t.File, filename: string): Record<string, TemplateNo
const elementId = idAttr.value.value;

const dynamicType = getDynamicTypeInfo(path);
const coreElementType = getCoreElementInfo(path);

mapping[elementId] = getTemplateNode(path, filename, componentStack, dynamicType);
mapping[elementId] = getTemplateNode(
path,
filename,
componentStack,
dynamicType,
coreElementType,
);
}
},
});
Expand Down
8 changes: 4 additions & 4 deletions apps/studio/electron/preload/webview/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { getDomElementByDomId, getElementAtLoc, updateElementInstance } from './
import {
getActionElementByDomId,
getActionLocation,
setDynamicElementType,
getDynamicElementType,
getElementType,
setElementType,
} from './elements/dom/helpers';
import { getInsertLocation } from './elements/dom/insert';
import { getRemoveActionFromDomId } from './elements/dom/remove';
Expand All @@ -28,8 +28,8 @@ export function setApi() {
// Elements
getElementAtLoc,
getDomElementByDomId,
setDynamicElementType,
getDynamicElementType,
setElementType,
getElementType,

// Actions
getActionLocation,
Expand Down
27 changes: 21 additions & 6 deletions apps/studio/electron/preload/webview/elements/dom/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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';
import type { CoreElementType, DynamicType } from '@onlook/models/element';

export function getActionElementByDomId(domId: string): ActionElement | null {
const el = elementFromDomId(domId);
Expand Down Expand Up @@ -80,22 +80,37 @@ export function getActionLocation(domId: string): ActionLocation | null {
};
}

export function getDynamicElementType(domId: string): DynamicType | null {
export function getElementType(domId: string): {
dynamicType: DynamicType | null;
coreType: CoreElementType | 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 { dynamicType: null, coreType: null };
}

return el.getAttribute(EditorAttributes.DATA_ONLOOK_DYNAMIC_TYPE) as DynamicType;
const dynamicType =
(el.getAttribute(EditorAttributes.DATA_ONLOOK_DYNAMIC_TYPE) as DynamicType) || null;
const coreType =
(el.getAttribute(EditorAttributes.DATA_ONLOOK_CORE_ELEMENT_TYPE) as CoreElementType) ||
null;

return { dynamicType, coreType };
}

export function setDynamicElementType(domId: string, dynamicType: string) {
export function setElementType(domId: string, dynamicType: string, coreElementType: string) {
const el = document.querySelector(`[${EditorAttributes.DATA_ONLOOK_DOM_ID}="${domId}"]`);

if (el) {
el.setAttribute(EditorAttributes.DATA_ONLOOK_DYNAMIC_TYPE, dynamicType);
if (dynamicType) {
el.setAttribute(EditorAttributes.DATA_ONLOOK_DYNAMIC_TYPE, dynamicType);
}
if (coreElementType) {
el.setAttribute(EditorAttributes.DATA_ONLOOK_CORE_ELEMENT_TYPE, coreElementType);
}
}
}
31 changes: 25 additions & 6 deletions apps/studio/src/lib/editor/engine/ast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,35 @@ export class AstManager {
return;
}

// Check if node needs type assignment
const hasSpecialType = templateNode.dynamicType || templateNode.coreElementType;
if (!hasSpecialType) {
this.findNodeInstance(webviewId, node, node, templateNode);
return;
}

const webview = this.editorEngine.webviews.getWebview(webviewId);
if (!webview) {
console.warn('Failed: Webview not found');
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}')`,
);
}
}

if (templateNode.coreElementType) {
node.coreElementType = templateNode.coreElementType;
}

webview.executeJavaScript(
`window.api?.setElementType(
'${node.domId}',
${templateNode.dynamicType ? `'${templateNode.dynamicType}'` : 'undefined'},
${templateNode.coreElementType ? `'${templateNode.coreElementType}'` : 'undefined'}
)`,
);

this.findNodeInstance(webviewId, node, node, templateNode);
}

Expand Down
18 changes: 14 additions & 4 deletions apps/studio/src/lib/editor/engine/element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,24 @@ export class ElementManager {
return;
}

const dynamicElementType = await webview.executeJavaScript(
`window.api?.getDynamicElementType('${selectedEl.domId}')`,
const { dynamicType, coreType } = await webview.executeJavaScript(
`window.api?.getElementType('${selectedEl.domId}')`,
);

if (dynamicElementType) {
if (coreType) {
toast({
title: 'Invalid Action',
description: `This element is part of a react expression (${dynamicElementType}) and cannot be deleted`,
description: `This element is a core element (${coreType}) and cannot be deleted`,
variant: 'destructive',
});

return;
}

if (dynamicType) {
toast({
title: 'Invalid Action',
description: `This element is part of a react expression (${dynamicType}) and cannot be deleted`,
variant: 'destructive',
});

Expand Down
1 change: 1 addition & 0 deletions packages/models/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum EditorAttributes {
DATA_ONLOOK_NEW_INDEX = 'data-onlook-new-index',
DATA_ONLOOK_EDITING_TEXT = 'data-onlook-editing-text',
DATA_ONLOOK_DYNAMIC_TYPE = 'data-onlook-dynamic-type',
DATA_ONLOOK_CORE_ELEMENT_TYPE = 'data-onlook-core-element-type',
}

export enum WebviewChannels {
Expand Down
3 changes: 3 additions & 0 deletions packages/models/src/element/layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { z } from 'zod';

export const DynamicTypeEnum = z.enum(['array', 'conditional', 'unknown']);
export type DynamicType = z.infer<typeof DynamicTypeEnum>;
export const CoreElementTypeEnum = z.enum(['component-root', 'body-tag']);
export type CoreElementType = z.infer<typeof CoreElementTypeEnum>;

const LayerNodeSchema = z.object({
domId: z.string(),
Expand All @@ -12,6 +14,7 @@ const LayerNodeSchema = z.object({
tagName: z.string(),
isVisible: z.boolean(),
dynamicType: DynamicTypeEnum.optional(),
coreElementType: CoreElementTypeEnum.optional(),
component: z.string().nullable(),
children: z.array(z.string()).nullable(),
parent: z.string().nullable(),
Expand Down
3 changes: 2 additions & 1 deletion packages/models/src/element/templateNode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { DynamicTypeEnum } from './layers';
import { CoreElementTypeEnum, DynamicTypeEnum } from './layers';

export const TemplateTagPositionSchema = z.object({
line: z.number(),
Expand All @@ -17,6 +17,7 @@ export const TemplateNodeSchema = z.object({
endTag: TemplateTagSchema.nullable(),
component: z.string().nullable(),
dynamicType: DynamicTypeEnum.nullable().optional(),
coreElementType: CoreElementTypeEnum.nullable().optional(),
});

export type TemplateNode = z.infer<typeof TemplateNodeSchema>;
Expand Down

0 comments on commit 84715f5

Please sign in to comment.