diff --git a/apps/builder/app/builder/features/settings-panel/controls/boolean.tsx b/apps/builder/app/builder/features/settings-panel/controls/boolean.tsx index d26af2278f..0ade338e80 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/boolean.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/boolean.tsx @@ -19,8 +19,9 @@ export const BooleanControl = ({ readOnly, onChange, onDelete, -}: ControlProps<"boolean", "boolean" | "expression">) => { +}: ControlProps<"boolean">) => { const id = useId(); + const label = getLabel(meta, propName); const { scope, aliases } = useStore($selectedInstanceScope); const expression = prop?.type === "expression" ? prop.value : JSON.stringify(computedValue); @@ -38,7 +39,7 @@ export const BooleanControl = ({ gap="2" > { + if (value !== undefined && typeof value !== "boolean") { + return `${label} expects a boolean value`; + } + }} value={expression} onChange={(newExpression) => onChange({ type: "expression", value: newExpression }) diff --git a/apps/builder/app/builder/features/settings-panel/controls/check.tsx b/apps/builder/app/builder/features/settings-panel/controls/check.tsx index 85a523dd13..b72a466fbe 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/check.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/check.tsx @@ -40,10 +40,7 @@ export const CheckControl = ({ readOnly, onChange, onDelete, -}: ControlProps< - "check" | "inline-check" | "multi-select", - "string[]" | "expression" ->) => { +}: ControlProps<"check" | "inline-check" | "multi-select">) => { const value = Array.isArray(computedValue) ? computedValue.map((item) => String(item)) : []; @@ -52,6 +49,7 @@ export const CheckControl = ({ const options = Array.from(new Set([...meta.options, ...value])); const id = useId(); + const label = getLabel(meta, propName); const { scope, aliases } = useStore($selectedInstanceScope); const expression = prop?.type === "expression" ? prop.value : JSON.stringify(computedValue); @@ -64,7 +62,7 @@ export const CheckControl = ({ description={meta.description} readOnly={readOnly} > - {getLabel(meta, propName)} + {label} } deletable={deletable} @@ -94,6 +92,14 @@ export const CheckControl = ({ { + const valid = + Array.isArray(value) && + value.every((item) => typeof item === "string"); + if (value !== undefined && valid === false) { + return `${label} expects an array of strings`; + } + }} value={expression} onChange={(newExpression) => onChange({ type: "expression", value: newExpression }) diff --git a/apps/builder/app/builder/features/settings-panel/controls/code.tsx b/apps/builder/app/builder/features/settings-panel/controls/code.tsx index ad25477d11..d5ae289147 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/code.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/code.tsx @@ -21,7 +21,7 @@ export const CodeControl = ({ readOnly, onChange, onDelete, -}: ControlProps<"code", "string" | "expression">) => { +}: ControlProps<"code">) => { const metaOverride = { ...meta, control: "text" as const, @@ -65,6 +65,11 @@ export const CodeControl = ({ { + if (value !== undefined && typeof value !== "string") { + return `${label} expects a string value`; + } + }} value={expression} onChange={(newExpression) => onChange({ type: "expression", value: newExpression }) diff --git a/apps/builder/app/builder/features/settings-panel/controls/combined.tsx b/apps/builder/app/builder/features/settings-panel/controls/combined.tsx index d72699fd67..7092c9a321 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/combined.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/combined.tsx @@ -14,7 +14,7 @@ export const renderControl = ({ meta, prop, ...rest -}: ControlProps & { key?: string }) => { +}: ControlProps & { key?: string }) => { const computed = rest.computedValue; // never render parameter props @@ -27,107 +27,53 @@ export const renderControl = ({ return; } - if ( - meta.control === "json" && - (prop === undefined || - prop.type === "json" || - (prop.type === "expression" && typeof computed === "object")) - ) { + if (meta.control === "json") { return ; } - if ( - meta.control === "text" && - (prop === undefined || - prop.type === "string" || - (prop.type === "expression" && typeof computed === "string")) - ) { + if (meta.control === "text") { return ; } - if ( - meta.control === "code" && - (prop === undefined || - prop.type === "string" || - (prop.type === "expression" && typeof computed === "string")) - ) { + if (meta.control === "code") { return ; } - if ( - meta.control === "color" && - (prop === undefined || prop.type === "string") - ) { + if (meta.control === "color") { return ( ); } - if ( - meta.control === "number" && - (prop === undefined || - prop.type === "number" || - (prop.type === "expression" && typeof computed === "number")) - ) { + if (meta.control === "number") { return ; } - if ( - meta.control === "boolean" && - (prop === undefined || - prop.type === "boolean" || - (prop.type === "expression" && typeof computed === "boolean")) - ) { + if (meta.control === "boolean") { return ; } if ( - (meta.control === "check" || - meta.control === "inline-check" || - meta.control === "multi-select") && - (prop === undefined || - prop.type === "string[]" || - (prop.type === "expression" && typeof computed === "object")) + meta.control === "check" || + meta.control === "inline-check" || + meta.control === "multi-select" ) { return ; } - if ( - (meta.control === "radio" || meta.control === "inline-radio") && - (prop === undefined || - prop.type === "string" || - (prop.type === "expression" && typeof computed === "string")) - ) { + if (meta.control === "radio" || meta.control === "inline-radio") { return ; } - if ( - meta.control === "select" && - (prop === undefined || - prop.type === "string" || - (prop.type === "expression" && typeof computed === "string")) - ) { + if (meta.control === "select") { return ; } - if ( - meta.control === "file" && - (prop === undefined || - prop.type === "asset" || - prop.type === "string" || - (prop.type === "expression" && typeof computed === "string")) - ) { + if (meta.control === "file") { return ; } - if ( - meta.control === "url" && - (prop === undefined || - prop.type === "string" || - prop.type === "page" || - prop.type === "asset" || - (prop.type === "expression" && typeof computed === "string")) - ) { + if (meta.control === "url") { return ; } diff --git a/apps/builder/app/builder/features/settings-panel/controls/file.tsx b/apps/builder/app/builder/features/settings-panel/controls/file.tsx index 3902cbe431..a00ba71283 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/file.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/file.tsx @@ -56,7 +56,7 @@ export const FileControl = ({ deletable, onChange, onDelete, -}: ControlProps<"file", "asset" | "string" | "expression">) => { +}: ControlProps<"file">) => { const id = useId(); const localStringValue = useLocalValue( @@ -78,6 +78,7 @@ export const FileControl = ({ } ); + const label = getLabel(meta, propName); const { scope, aliases } = useStore($selectedInstanceScope); const expression = prop?.type === "expression" ? prop.value : JSON.stringify(computedValue); @@ -86,7 +87,7 @@ export const FileControl = ({ - {getLabel(meta, propName)} + {label} } deletable={deletable} @@ -97,6 +98,11 @@ export const FileControl = ({ { + if (value !== undefined && typeof value !== "string") { + return `${label} expects a string value or file`; + } + }} value={expression} onChange={(newExpression) => onChange({ type: "expression", value: newExpression }) diff --git a/apps/builder/app/builder/features/settings-panel/controls/json.tsx b/apps/builder/app/builder/features/settings-panel/controls/json.tsx index 7407f231d0..9cdda57d8a 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/json.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/json.tsx @@ -24,7 +24,7 @@ export const JsonControl = ({ readOnly, onChange, onDelete, -}: ControlProps<"json", "json" | "expression">) => { +}: ControlProps<"json">) => { const valueString = formatValue(computedValue ?? ""); const localValue = useLocalValue(valueString, (value) => { try { diff --git a/apps/builder/app/builder/features/settings-panel/controls/number.tsx b/apps/builder/app/builder/features/settings-panel/controls/number.tsx index de5ae6d79a..33f8b7e532 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/number.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/number.tsx @@ -21,7 +21,7 @@ export const NumberControl = ({ deletable, readOnly, onDelete, -}: ControlProps<"number", "number" | "expression">) => { +}: ControlProps<"number">) => { const id = useId(); const [isInvalid, setIsInvalid] = useState(false); @@ -42,6 +42,7 @@ export const NumberControl = ({ } ); + const label = getLabel(meta, propName); const { scope, aliases } = useStore($selectedInstanceScope); const expression = prop?.type === "expression" ? prop.value : JSON.stringify(computedValue); @@ -50,7 +51,7 @@ export const NumberControl = ({ - {getLabel(meta, propName)} + {label} } deletable={deletable} @@ -77,6 +78,11 @@ export const NumberControl = ({ { + if (value !== undefined && typeof value !== "number") { + return `${label} expects a number value`; + } + }} value={expression} onChange={(newExpression) => onChange({ type: "expression", value: newExpression }) diff --git a/apps/builder/app/builder/features/settings-panel/controls/radio.tsx b/apps/builder/app/builder/features/settings-panel/controls/radio.tsx index b07ba8a65b..e1a910f607 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/radio.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/radio.tsx @@ -27,7 +27,7 @@ export const RadioControl = ({ readOnly, onChange, onDelete, -}: ControlProps<"radio" | "inline-radio", "string" | "expression">) => { +}: ControlProps<"radio" | "inline-radio">) => { const value = computedValue === undefined ? undefined : String(computedValue); // making sure that the current value is in the list of options const options = @@ -36,6 +36,7 @@ export const RadioControl = ({ : [value, ...meta.options]; const id = useId(); + const label = getLabel(meta, propName); const { scope, aliases } = useStore($selectedInstanceScope); const expression = prop?.type === "expression" ? prop.value : JSON.stringify(computedValue); @@ -44,7 +45,7 @@ export const RadioControl = ({ - {getLabel(meta, propName)} + {label} } deletable={deletable} @@ -73,6 +74,18 @@ export const RadioControl = ({ { + if ( + value !== undefined && + meta.options.includes(String(value)) === false + ) { + const formatter = new Intl.ListFormat(undefined, { + type: "disjunction", + }); + const options = formatter.format(meta.options); + return `${label} expects one of ${options}`; + } + }} value={expression} onChange={(newExpression) => onChange({ type: "expression", value: newExpression }) diff --git a/apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx b/apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx index 7114d9f723..63f969bde6 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx @@ -7,12 +7,13 @@ import { SmallIconButton, Text, } from "@webstudio-is/design-system"; +import { TrashIcon } from "@webstudio-is/icons"; +import type { Prop } from "@webstudio-is/sdk"; import { assetsStore } from "~/shared/nano-states"; import { FloatingPanel } from "~/builder/shared/floating-panel"; import { ImageManager } from "~/builder/shared/image-manager"; import { type ControlProps } from "../shared"; import { acceptToMimeCategories } from "@webstudio-is/asset-uploader"; -import { TrashIcon } from "@webstudio-is/icons"; // tests whether we can use ImageManager for the given "accept" value const isImageAccept = (accept?: string) => { @@ -23,11 +24,11 @@ const isImageAccept = (accept?: string) => { ); }; -type AssetControlProps = ControlProps; +type AssetControlProps = ControlProps; type Props = { accept?: string; - prop: AssetControlProps["prop"]; + prop?: Extract; onChange: AssetControlProps["onChange"]; onDelete: AssetControlProps["onDelete"]; }; diff --git a/apps/builder/app/builder/features/settings-panel/controls/select.tsx b/apps/builder/app/builder/features/settings-panel/controls/select.tsx index ad6306176a..d5841650a6 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/select.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/select.tsx @@ -21,7 +21,7 @@ export const SelectControl = ({ readOnly, onChange, onDelete, -}: ControlProps<"select", "string" | "expression">) => { +}: ControlProps<"select">) => { const id = useId(); const value = computedValue === undefined ? undefined : String(computedValue); @@ -31,6 +31,7 @@ export const SelectControl = ({ ? meta.options : [value, ...meta.options]; + const label = getLabel(meta, propName); const { scope, aliases } = useStore($selectedInstanceScope); const expression = prop?.type === "expression" ? prop.value : JSON.stringify(computedValue); @@ -39,7 +40,7 @@ export const SelectControl = ({ - {getLabel(meta, propName)} + {label} } deletable={deletable} @@ -64,6 +65,18 @@ export const SelectControl = ({ { + if ( + value !== undefined && + meta.options.includes(String(value)) === false + ) { + const formatter = new Intl.ListFormat(undefined, { + type: "disjunction", + }); + const options = formatter.format(meta.options); + return `${label} expects one of ${options}`; + } + }} value={expression} onChange={(newExpression) => onChange({ type: "expression", value: newExpression }) diff --git a/apps/builder/app/builder/features/settings-panel/controls/text.tsx b/apps/builder/app/builder/features/settings-panel/controls/text.tsx index 71968c3cf7..0e5674c510 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/text.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/text.tsx @@ -21,7 +21,7 @@ export const TextControl = ({ readOnly, onChange, onDelete, -}: ControlProps<"text", "string" | "expression">) => { +}: ControlProps<"text">) => { const localValue = useLocalValue(String(computedValue ?? ""), (value) => { if (prop?.type === "expression") { updateExpressionValue(prop.value, value); @@ -55,6 +55,11 @@ export const TextControl = ({ { + if (value !== undefined && typeof value !== "string") { + return `${label} expects a string value`; + } + }} value={expression} onChange={(newExpression) => onChange({ type: "expression", value: newExpression }) diff --git a/apps/builder/app/builder/features/settings-panel/controls/url.tsx b/apps/builder/app/builder/features/settings-panel/controls/url.tsx index c6d823fab2..dea09e7702 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/url.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/url.tsx @@ -34,10 +34,7 @@ import { } from "../shared"; import { SelectAsset } from "./select-asset"; -type UrlControlProps = ControlProps< - "url", - "string" | "page" | "asset" | "expression" ->; +type UrlControlProps = ControlProps<"url">; type BaseControlProps = { id: string; @@ -444,6 +441,7 @@ export const UrlControl = ({ const BaseControl = modes[mode].control; + const label = getLabel(meta, propName); const { scope, aliases } = useStore($selectedInstanceScope); const expression = prop?.type === "expression" ? prop.value : JSON.stringify(computedValue); @@ -452,7 +450,7 @@ export const UrlControl = ({ - {getLabel(meta, propName)} + {label} } deletable={deletable} @@ -498,6 +496,11 @@ export const UrlControl = ({ { + if (value !== undefined && typeof value !== "string") { + return `${label} expects a string value, page or file`; + } + }} value={expression} onChange={(newExpression) => onChange({ type: "expression", value: newExpression }) diff --git a/apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx b/apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx index 3545325631..cbb3ff9a51 100644 --- a/apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx +++ b/apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx @@ -1,4 +1,4 @@ -import { Fragment, useState } from "react"; +import { useState } from "react"; import { useStore } from "@nanostores/react"; import type { Instance } from "@webstudio-is/sdk"; import { @@ -98,55 +98,48 @@ const renderProperty = ( }: PropsSectionProps, { prop, propName, meta, readOnly }: PropAndMeta, deletable?: boolean -) => ( - // fix the issue with changing type while binding expression - // old prop value is getting preserved in useLocalValue and saved into variable - // when unmount to reproduce try to edit body id prop - // and then bind json variable to it - - {renderControl({ - key: propName, - instanceId, - meta, - prop, - computedValue: propValues.get(propName), - propName, - readOnly, - deletable: deletable ?? false, - onDelete: () => { - if (prop) { - logic.handleDelete(prop); - } - }, - onChange: (propValue, asset) => { - logic.handleChange({ prop, propName }, propValue); +) => + renderControl({ + key: propName, + instanceId, + meta, + prop, + computedValue: propValues.get(propName), + propName, + readOnly, + deletable: deletable ?? false, + onDelete: () => { + if (prop) { + logic.handleDelete(prop); + } + }, + onChange: (propValue, asset) => { + logic.handleChange({ prop, propName }, propValue); - // @todo: better way to do this? - if ( - component === "Image" && - propName === "src" && - asset && - "width" in asset.meta && - "height" in asset.meta - ) { - logic.handleChangeByPropName("width", { - value: asset.meta.width, - type: "number", - }); - logic.handleChangeByPropName("height", { - value: asset.meta.height, - type: "number", - }); + // @todo: better way to do this? + if ( + component === "Image" && + propName === "src" && + asset && + "width" in asset.meta && + "height" in asset.meta + ) { + logic.handleChangeByPropName("width", { + value: asset.meta.width, + type: "number", + }); + logic.handleChangeByPropName("height", { + value: asset.meta.height, + type: "number", + }); - setCssProperty("height")({ - type: "keyword", - value: "fit-content", - }); - } - }, - })} - -); + setCssProperty("height")({ + type: "keyword", + value: "fit-content", + }); + } + }, + }); const AddPropertyForm = ({ availableProps, diff --git a/apps/builder/app/builder/features/settings-panel/shared.tsx b/apps/builder/app/builder/features/settings-panel/shared.tsx index 372c85d0fc..5e7d708005 100644 --- a/apps/builder/app/builder/features/settings-panel/shared.tsx +++ b/apps/builder/app/builder/features/settings-panel/shared.tsx @@ -52,16 +52,12 @@ type PropMetaByControl = Control extends string ? Extract : never; -type PropByType = Type extends string - ? Extract - : never; - -export type ControlProps = { +export type ControlProps = { instanceId: string; meta: PropMetaByControl; // prop is optional because we don't have it when an intial prop is not set // and we don't want to show user something like a 0 for number when it's in fact not set to any value - prop: PropByType | undefined; + prop: Prop | undefined; propName: string; computedValue: unknown; deletable: boolean; diff --git a/apps/builder/app/builder/shared/binding-popover.tsx b/apps/builder/app/builder/shared/binding-popover.tsx index 9952091172..36413299ed 100644 --- a/apps/builder/app/builder/shared/binding-popover.tsx +++ b/apps/builder/app/builder/shared/binding-popover.tsx @@ -1,4 +1,10 @@ -import { forwardRef, useMemo, useRef, useState } from "react"; +import { + forwardRef, + useMemo, + useRef, + useState, + type ButtonHTMLAttributes, +} from "react"; import { isFeatureEnabled } from "@webstudio-is/feature-flags"; import { DotIcon, PlusIcon, TrashIcon } from "@webstudio-is/icons"; import { @@ -75,25 +81,26 @@ const getUsedIdentifiers = (expression: string) => { const BindingPanel = ({ scope, aliases, - error, + valueError, value, onChange, - onPotentialErrorChange, - onValidate, + onSave, }: { scope: Record; aliases: Map; - error: undefined | string; + valueError?: string; value: string; - onChange: (newValue: string) => void; - onPotentialErrorChange: (error: string) => void; - onValidate: () => void; + onChange: () => void; + onSave: (value: string, invalid: boolean) => void; }) => { const [expression, setExpression] = useState(value); const usedIdentifiers = useMemo(() => getUsedIdentifiers(value), [value]); + const [error, setError] = useState(); + const [touched, setTouched] = useState(false); const updateExpression = (newExpression: string) => { setExpression(newExpression); + onChange(); try { if (newExpression.trim().length === 0) { throw Error("Cannot use empty expression"); @@ -107,9 +114,9 @@ const BindingPanel = ({ return identifier; }, }); - onChange(newExpression); + setError(undefined); } catch (error) { - onPotentialErrorChange((error as Error).message); + setError((error as Error).message); } }; @@ -139,8 +146,8 @@ const BindingPanel = ({ active={usedIdentifiers.has(identifier)} // convert variable to expression onClick={() => { - setExpression(identifier); - onChange(identifier); + updateExpression(identifier); + onSave(identifier, false); }} /> ); @@ -148,15 +155,30 @@ const BindingPanel = ({ - +
{ + updateExpression(value); + setTouched(false); + }} + onBlur={() => { + onSave(expression, error !== undefined); + setTouched(true); + }} />
@@ -165,107 +187,137 @@ const BindingPanel = ({ ); }; -const BindingButton = forwardRef((props, ref) => ( - & { error?: string } +>(({ error, ...props }, ref) => { + const expanded = props["aria-expanded"]; + return ( + // prevent giving content to tooltip when popover is open + // to avoid button remounting and popover flickering + // when switch between valid and error value + + - - -
- } - /> -)); + {...props} + icon={ + + + + + } + /> + + ); +}); BindingButton.displayName = "BindingButton"; export const BindingPopover = ({ scope, aliases, + validate, value, onChange, onRemove, }: { scope: Record; aliases: Map; - acceptableType?: "text" | "any"; + validate?: (value: unknown) => undefined | string; value: string; onChange: (newValue: string) => void; onRemove: (evaluatedValue: unknown) => void; }) => { const [open, onOpenChange] = useState(false); - const [error, setError] = useState(); - const potentialError = useRef(); + const hasUnsavedChange = useRef(false); + const preventedClosing = useRef(false); if (isFeatureEnabled("bindings") === false) { return; } + const valueError = validate?.(evaluateExpressionWithinScope(value, scope)); return ( { - if (potentialError.current) { - setError(potentialError.current); - } else { - onOpenChange(newOpen); + // handle special case for popover close + if (newOpen === false) { + // prevent saving when changes are not saved or validated + if (hasUnsavedChange.current) { + // schedule closing after saving + preventedClosing.current = true; + return; + } + preventedClosing.current = false; } + onOpenChange(newOpen); }} > - + { - potentialError.current = undefined; - setError(undefined); - onChange(newValue); + onChange={() => { + hasUnsavedChange.current = true; }} - onPotentialErrorChange={(error) => { - potentialError.current = error; - }} - onValidate={() => { - if (potentialError.current) { - setError(potentialError.current); + onSave={(value, invalid) => { + // avoid saving without changes + if (hasUnsavedChange.current === false) { + return; + } + // let user see the error and let close popover after + hasUnsavedChange.current = false; + if (invalid) { + return; + } + // save value and close popover + onChange(value); + if (preventedClosing.current) { + preventedClosing.current = false; + onOpenChange(false); } }} /> @@ -279,15 +331,16 @@ export const BindingPopover = ({ aria-label="Reset binding" prefix={} color="ghost" - onClick={() => { - // reset error, inline variables and close dialog - potentialError.current = undefined; - setError(undefined); + onClick={(event) => { + event.preventDefault(); + // inline variables and close dialog const evaluatedValue = evaluateExpressionWithinScope( value, scope ); onRemove(evaluatedValue); + preventedClosing.current = false; + hasUnsavedChange.current = false; onOpenChange(false); }} /> diff --git a/apps/builder/app/builder/shared/code-editor.tsx b/apps/builder/app/builder/shared/code-editor.tsx index 8fdcbc0700..214644d724 100644 --- a/apps/builder/app/builder/shared/code-editor.tsx +++ b/apps/builder/app/builder/shared/code-editor.tsx @@ -38,6 +38,7 @@ const rootStyle = css({ export const CodeEditor = ({ extensions, + className, readOnly = false, autoFocus = false, value, @@ -45,6 +46,7 @@ export const CodeEditor = ({ onBlur, }: { extensions: Extension[]; + className?: string; readOnly?: boolean; autoFocus?: boolean; value: string; @@ -130,5 +132,9 @@ export const CodeEditor = ({ }); }, [value]); - return
; + let rootClassName = rootStyle.toString(); + if (className) { + rootClassName += ` ${className}`; + } + return
; }; diff --git a/apps/builder/app/builder/shared/expression-editor.tsx b/apps/builder/app/builder/shared/expression-editor.tsx index 62a1bfbc5b..1c5643b265 100644 --- a/apps/builder/app/builder/shared/expression-editor.tsx +++ b/apps/builder/app/builder/shared/expression-editor.tsx @@ -212,9 +212,14 @@ const autocompletionStyle = css({ const emptyScope: Scope = {}; const emptyAliases: Aliases = new Map(); +const errorStyle = css({ + borderColor: theme.colors.borderDestructiveMain, +}); + export const ExpressionEditor = ({ scope = emptyScope, aliases = emptyAliases, + color, autoFocus = false, readOnly = false, value, @@ -229,6 +234,7 @@ export const ExpressionEditor = ({ * variable aliases to show instead of $ws$dataSource$id */ aliases?: Aliases; + color?: "error"; autoFocus?: boolean; readOnly?: boolean; value: string; @@ -275,6 +281,7 @@ export const ExpressionEditor = ({ return (