From 9f9330bc81f9e8f2bda49de69531f4d825e98d6c Mon Sep 17 00:00:00 2001
From: Nangelov7 <25034472+Nangelov7@users.noreply.github.com>
Date: Sat, 5 Jul 2025 21:26:21 +0300
Subject: [PATCH 1/2] feat: add code editor component
---
client/package.json | 2 +
.../lowcoder-design/src/icons/index.tsx | 2 +
.../src/icons/v2/code-editor-m.svg | 15 +
.../src/icons/v2/code-editor-s.svg | 15 +
.../comps/codeEditorComp/codeEditorComp.tsx | 461 ++++++++++++++++++
.../lowcoder/src/comps/index-test.tsx | 16 +
client/packages/lowcoder/src/comps/index.tsx | 18 +-
.../lowcoder/src/comps/uiCompRegistry.ts | 1 +
.../packages/lowcoder/src/i18n/locales/en.ts | 69 ++-
.../src/pages/editor/editorConstants.tsx | 2 +
client/yarn.lock | 38 ++
.../embedd-an-app/native-embed-sdk/README.md | 1 +
12 files changed, 638 insertions(+), 2 deletions(-)
create mode 100644 client/packages/lowcoder-design/src/icons/v2/code-editor-m.svg
create mode 100644 client/packages/lowcoder-design/src/icons/v2/code-editor-s.svg
create mode 100644 client/packages/lowcoder/src/comps/comps/codeEditorComp/codeEditorComp.tsx
diff --git a/client/package.json b/client/package.json
index 12f93a4aa7..8f2d98e0c5 100644
--- a/client/package.json
+++ b/client/package.json
@@ -73,6 +73,7 @@
},
"dependencies": {
"@lottiefiles/react-lottie-player": "^3.5.3",
+ "@monaco-editor/react": "^4.7.0",
"@remixicon/react": "^4.1.1",
"@supabase/supabase-js": "^2.45.4",
"@testing-library/react": "^14.1.2",
@@ -81,6 +82,7 @@
"antd-mobile": "^5.34.0",
"chalk": "4",
"flag-icons": "^7.2.1",
+ "monaco-editor": "^0.52.2",
"number-precision": "^1.6.0",
"react-countup": "^6.5.3",
"react-github-btn": "^1.4.0",
diff --git a/client/packages/lowcoder-design/src/icons/index.tsx b/client/packages/lowcoder-design/src/icons/index.tsx
index b033d52e92..96b75c985b 100644
--- a/client/packages/lowcoder-design/src/icons/index.tsx
+++ b/client/packages/lowcoder-design/src/icons/index.tsx
@@ -329,6 +329,7 @@ export { ReactComponent as RangeSliderCompIconSmall } from "./v2/range-slider-s.
export { ReactComponent as RatingCompIconSmall } from "./v2/rating-s.svg";
export { ReactComponent as ResponsiveLayoutCompIconSmall } from "./v2/resposive-layout-s.svg"; // new
export { ReactComponent as SplitLayoutCompIconSmall } from "./v2/split-layout-s.svg"; // new
+export { ReactComponent as CodeEditorCompIconSmall } from "./v2/code-editor-s.svg"; // new
export { ReactComponent as RichTextEditorCompIconSmall } from "./v2/rich-text-editor-s.svg"; // new
export { ReactComponent as ScannerCompIconSmall } from "./v2/scanner-s.svg"; // new
export { ReactComponent as ShapesCompIconSmall } from "./v2/shapes-s.svg"; // new
@@ -437,6 +438,7 @@ export { ReactComponent as RangeSliderCompIcon } from "./v2/range-slider-m.svg";
export { ReactComponent as RatingCompIcon } from "./v2/rating-m.svg";
export { ReactComponent as ResponsiveLayoutCompIcon } from "./v2/resposive-layout-m.svg";
export { ReactComponent as SplitLayoutCompIcon } from "./v2/split-layout-m.svg";
+export { ReactComponent as CodeEditorCompIcon } from "./v2/code-editor-m.svg";
export { ReactComponent as RichTextEditorCompIcon } from "./v2/rich-text-editor-m.svg";
export { ReactComponent as ScannerCompIcon } from "./v2/scanner-m.svg";
export { ReactComponent as ShapesCompIcon } from "./v2/shapes-m.svg";
diff --git a/client/packages/lowcoder-design/src/icons/v2/code-editor-m.svg b/client/packages/lowcoder-design/src/icons/v2/code-editor-m.svg
new file mode 100644
index 0000000000..95615f9ea3
--- /dev/null
+++ b/client/packages/lowcoder-design/src/icons/v2/code-editor-m.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/client/packages/lowcoder-design/src/icons/v2/code-editor-s.svg b/client/packages/lowcoder-design/src/icons/v2/code-editor-s.svg
new file mode 100644
index 0000000000..ebbeed86d3
--- /dev/null
+++ b/client/packages/lowcoder-design/src/icons/v2/code-editor-s.svg
@@ -0,0 +1,15 @@
+
+
diff --git a/client/packages/lowcoder/src/comps/comps/codeEditorComp/codeEditorComp.tsx b/client/packages/lowcoder/src/comps/comps/codeEditorComp/codeEditorComp.tsx
new file mode 100644
index 0000000000..0a9b731b05
--- /dev/null
+++ b/client/packages/lowcoder/src/comps/comps/codeEditorComp/codeEditorComp.tsx
@@ -0,0 +1,461 @@
+import {
+ UICompBuilder,
+ NameConfig,
+ Section,
+ withDefault,
+ withExposingConfigs,
+ withMethodExposing,
+ eventHandlerControl,
+ stringExposingStateControl,
+ BoolControl,
+ dropdownControl,
+ AutoHeightControl,
+} from "lowcoder-sdk";
+import { useResizeDetector } from "react-resize-detector";
+import Editor from "@monaco-editor/react";
+import { styled } from "styled-components";
+import { trans } from "i18n";
+import { useRef, useCallback, useLayoutEffect } from "react";
+import debounce from "lodash/debounce";
+import * as monacoEditor from "monaco-editor";
+
+const CodeEditorWrapper = styled.div`
+ border: 1px solid #dddddd;
+`;
+
+let CodeEditorTmpComp = (function () {
+
+ const languages = [
+ { label: trans("codeEditor.languages.yaml"), value: "yaml" },
+ { label: trans("codeEditor.languages.json"), value: "json" },
+ { label: trans("codeEditor.languages.xml"), value: "xml" },
+ { label: trans("codeEditor.languages.html"), value: "html" },
+ { label: trans("codeEditor.languages.css"), value: "css" },
+ { label: trans("codeEditor.languages.ini"), value: "ini" },
+ { label: trans("codeEditor.languages.sql"), value: "sql" },
+ { label: trans("codeEditor.languages.php"), value: "php" },
+ { label: trans("codeEditor.languages.shell"), value: "shell" },
+ { label: trans("codeEditor.languages.powershell"), value: "powershell" },
+ { label: trans("codeEditor.languages.handlebars"), value: "handlebars" },
+ { label: trans("codeEditor.languages.dockerfile"), value: "dockerfile" },
+ { label: trans("codeEditor.languages.graphql"), value: "graphql" },
+ { label: trans("codeEditor.languages.markdown"), value: "markdown" },
+ { label: trans("codeEditor.languages.plaintext"), value: "plaintext" },
+ { label: trans("codeEditor.languages.python"), value: "python" },
+ { label: trans("codeEditor.languages.ruby"), value: "ruby" },
+ { label: trans("codeEditor.languages.rust"), value: "rust" },
+ { label: trans("codeEditor.languages.java"), value: "java" },
+ { label: trans("codeEditor.languages.c"), value: "c" },
+ { label: trans("codeEditor.languages.csharp"), value: "csharp" },
+ { label: trans("codeEditor.languages.cpp"), value: "cpp" },
+ { label: trans("codeEditor.languages.go"), value: "go" },
+ { label: trans("codeEditor.languages.javascript"), value: "javascript" },
+ { label: trans("codeEditor.languages.typescript"), value: "typescript" }
+ ].sort((a, b) => a.label.localeCompare(b.label))
+
+ const defaultValues = {
+ value: "",
+ language: "yaml",
+ theme: "light",
+ lineNumbers: "on",
+ lightbulb: monacoEditor.editor.ShowLightbulbIconMode.OnCode,
+ enabled: true,
+ disabled: false,
+ autoHeight: "445px",
+ }
+
+ const themes = [
+ { label: trans("codeEditor.theme.light"), value: "light" },
+ { label: trans("codeEditor.theme.dark"), value: "vs-dark" },
+ ].sort((a, b) => a.label.localeCompare(b.label))
+
+ const lineNumbersOptions = [
+ { label: trans("codeEditor.lineNumberOptions.on"), value: "on" },
+ { label: trans("codeEditor.lineNumberOptions.off"), value: "off" },
+ { label: trans("codeEditor.lineNumberOptions.interval"), value: "interval" },
+ { label: trans("codeEditor.lineNumberOptions.relative"), value: "relative" },
+ ].sort((a, b) => a.label.localeCompare(b.label))
+
+ const childrenMap = {
+ autoHeight: withDefault(AutoHeightControl, "auto"),
+ language: dropdownControl(languages, defaultValues.language),
+ theme: dropdownControl(themes, defaultValues.theme),
+ lineNumbers: dropdownControl(lineNumbersOptions, defaultValues.lineNumbers),
+ minimap: withDefault(BoolControl, defaultValues.enabled),
+ stickyScroll: withDefault(BoolControl, defaultValues.enabled),
+ lightbulb: withDefault(BoolControl, defaultValues.enabled),
+ hover: withDefault(BoolControl, defaultValues.enabled),
+ folding: withDefault(BoolControl, defaultValues.enabled),
+ readOnly: withDefault(BoolControl, defaultValues.disabled),
+ value: stringExposingStateControl("text", defaultValues.value),
+ onEvent: eventHandlerControl([
+ {
+ label: "onChange",
+ value: "change",
+ description: "Triggers when data changes",
+ },
+ ] as const),
+ };
+
+ return new UICompBuilder(childrenMap, (props) => {
+
+ const editorRef = useRef(null);
+ const lastExternalValue = useRef(props.value.value);
+
+ const { ref: conRef } = useResizeDetector({
+ onResize: () => {
+ if (editorRef.current) {
+ setTimeout(() => {
+ editorRef.current?.layout();
+ }, 0);
+ }
+ }
+ });
+
+ const getEffectiveDimensions = () => {
+ if (props.autoHeight) {
+ return {
+ width: "100%",
+ height: defaultValues.autoHeight
+ };
+ }
+ return {
+ width: "100%",
+ height: "100%"
+ };
+ };
+
+ const effectiveDimensions = getEffectiveDimensions();
+
+ const handleEditorDidMount = (
+ editor: monacoEditor.editor.IStandaloneCodeEditor
+ ) => {
+ editorRef.current = editor;
+ setTimeout(() => {
+ editor.layout();
+ }, 0);
+ };
+
+ const debouncedOnChange = useCallback(
+ debounce((value: string | undefined) => {
+ if(props.value && value !== undefined) {
+ lastExternalValue.current = value;
+ props.value.onChange(value);
+ props.onEvent("change");
+ }
+ }, 300),
+ [props.value, props.onEvent]
+ );
+
+ const handleOnChange = (value: string | undefined) => {
+ if (value !== undefined) {
+ debouncedOnChange(value);
+ }
+ };
+
+ useLayoutEffect(() => {
+ const editor = editorRef.current;
+ if (!editor) return;
+
+ const currentValue = editor.getValue();
+ const newValue = props.value.value;
+
+ if (newValue !== currentValue && newValue !== lastExternalValue.current) {
+ const position = editor.getPosition();
+ const scrollTop = editor.getScrollTop();
+
+ editor.setValue(newValue);
+ lastExternalValue.current = newValue;
+
+ if (position) {
+ editor.setPosition(position);
+ }
+ editor.setScrollTop(scrollTop);
+ }
+ }, [props.value.value]);
+
+ return (
+
+
+
+ );
+})
+.setPropertyViewFn((children: any) => {
+ return (
+ <>
+
+ {children.value.propertyView({ label: trans("codeEditor.properties.value") })}
+ {children.language.propertyView({ label: trans("codeEditor.properties.language") })}
+ {children.theme.propertyView({ label: trans("codeEditor.properties.theme") })}
+ {children.lineNumbers.propertyView({ label: trans("codeEditor.properties.lineNumbers") })}
+ {children.minimap.propertyView({ label: trans("codeEditor.properties.minimap") })}
+ {children.stickyScroll.propertyView({ label: trans("codeEditor.properties.stickyScroll")})}
+ {children.lightbulb.propertyView({ label: trans("codeEditor.properties.lightbulb") })}
+ {children.hover.propertyView({ label: trans("codeEditor.properties.hover") })}
+ {children.folding.propertyView({ label: trans("codeEditor.properties.folding") })}
+ {children.readOnly.propertyView({ label: trans("codeEditor.properties.readOnly") })}
+
+
+ {children.onEvent.propertyView()}
+
+
+ {children.autoHeight.getPropertyView()}
+
+ >
+ );
+})
+.build();
+})();
+
+CodeEditorTmpComp = class extends CodeEditorTmpComp {
+ autoHeight(): boolean {
+ return this.children.autoHeight.getView();
+ }
+};
+
+CodeEditorTmpComp = withMethodExposing(CodeEditorTmpComp, [
+ {
+ method: {
+ name: "setValue",
+ description: trans("codeEditor.methods.setValue"),
+ params: [{
+ name: "value",
+ type: "JSON",
+ description: "JSON value"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ let codeValue = Array.isArray(values) ? values[0] : values;
+ if (typeof codeValue === "object" && codeValue !== null) {
+ codeValue = JSON.stringify(codeValue, null, 2);
+ }
+ comp.children.value.dispatchChangeValueAction(codeValue);
+ }
+ },
+ {
+ method: {
+ name: "setLanguage",
+ description: trans("codeEditor.methods.setLanguage"),
+ params: [{
+ name: "language",
+ type: "string",
+ description: "string"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.language.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.language.dispatchChangeValueAction(values);
+ }
+ }
+ },
+ {
+ method: {
+ name: "setTheme",
+ description: trans("codeEditor.methods.setTheme"),
+ params: [{
+ name: "theme",
+ type: "string",
+ description: "string"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.theme.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.theme.dispatchChangeValueAction(values);
+ }
+ }
+ },
+ {
+ method: {
+ name: "setLineNumbers",
+ description: trans("codeEditor.methods.setLineNumbers"),
+ params: [{
+ name: "lineNumbers",
+ type: "string",
+ description: "string"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.lineNumbers.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.lineNumbers.dispatchChangeValueAction(values);
+ }
+ }
+ },
+ {
+ method: {
+ name: "enableMinimap",
+ description: trans("codeEditor.methods.enableMinimap"),
+ params: [{
+ name: "minimap",
+ type: "boolean",
+ description: "boolean"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.minimap.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.minimap.dispatchChangeValueAction(values);
+ }
+ }
+ },
+ {
+ method: {
+ name: "enableStickyScroll",
+ description: trans("codeEditor.methods.enableStickyScroll"),
+ params: [{
+ name: "stickyScroll",
+ type: "boolean",
+ description: "boolean"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.stickyScroll.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.stickyScroll.dispatchChangeValueAction(values);
+ }
+ }
+ },
+ {
+ method: {
+ name: "enableLightbulb",
+ description: trans("codeEditor.methods.enableLightbulb"),
+ params: [{
+ name: "lightbulb",
+ type: "boolean",
+ description: "boolean"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ const lightbulbEnum = monacoEditor.editor.ShowLightbulbIconMode;
+
+ if(Array.isArray(values)) {
+ if(Boolean(values[0])) {
+ comp.children.lightbulb.dispatchChangeValueAction(lightbulbEnum.OnCode);
+ } else {
+ comp.children.lightbulb.dispatchChangeValueAction(lightbulbEnum.Off);
+ }
+ } else {
+ if(Boolean(values)) {
+ comp.children.lightbulb.dispatchChangeValueAction(lightbulbEnum.OnCode);
+ } else {
+ comp.children.lightbulb.dispatchChangeValueAction(lightbulbEnum.Off);
+ }
+ }
+ }
+ },
+ {
+ method: {
+ name: "enableHover",
+ description: trans("codeEditor.methods.enableHover"),
+ params: [{
+ name: "hover",
+ type: "boolean",
+ description: "boolean"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.hover.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.hover.dispatchChangeValueAction(values);
+ }
+ }
+ },
+ {
+ method: {
+ name: "enableFolding",
+ description: trans("codeEditor.methods.enableFolding"),
+ params: [{
+ name: "folding",
+ type: "boolean",
+ description: "boolean"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.folding.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.folding.dispatchChangeValueAction(values);
+ }
+ }
+ },
+ {
+ method: {
+ name: "setReadOnly",
+ description: trans("codeEditor.methods.setReadOnly"),
+ params: [{
+ name: "readOnly",
+ type: "boolean",
+ description: "boolean"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.readOnly.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.readOnly.dispatchChangeValueAction(values);
+ }
+ }
+ },
+]);
+
+export const CodeEditorComp = withExposingConfigs(CodeEditorTmpComp, [
+ new NameConfig("value", trans("codeEditor.properties.value")),
+ new NameConfig("language", trans("codeEditor.properties.language")),
+ new NameConfig("theme", trans("codeEditor.properties.theme")),
+ new NameConfig("lineNumbers", trans("codeEditor.properties.lineNumbers")),
+ new NameConfig("minimap", trans("codeEditor.properties.minimap")),
+ new NameConfig("stickyScroll", trans("codeEditor.properties.stickyScroll")),
+ new NameConfig("lightbulb", trans("codeEditor.properties.lightbulb")),
+ new NameConfig("hover", trans("codeEditor.properties.hover")),
+ new NameConfig("folding", trans("codeEditor.properties.folding")),
+ new NameConfig("readOnly", trans("codeEditor.properties.readOnly")),
+]);
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/comps/index-test.tsx b/client/packages/lowcoder/src/comps/index-test.tsx
index b832d8a1b3..4c0770c2b4 100644
--- a/client/packages/lowcoder/src/comps/index-test.tsx
+++ b/client/packages/lowcoder/src/comps/index-test.tsx
@@ -139,6 +139,7 @@ import {
AutoCompleteCompIcon,
ResponsiveLayoutCompIcon,
MermaidCompIcon,
+ CodeEditorCompIcon
} from "lowcoder-design";
type Registry = {
@@ -582,6 +583,21 @@ var uiCompMap: Registry = {
h: 50,
},
},
+ codeEditor: {
+ name: trans("uiComp.codeEditorCompName"),
+ enName: "Code Editor",
+ categories: ["forms"],
+ description: trans("uiComp.codeEditorCompDesc"),
+ icon: CodeEditorCompIcon,
+ keywords: trans("uiComp.codeEditorCompKeywords"),
+ lazyLoad: true,
+ compName: "CodeEditorComp",
+ compPath: "comps/codeEditorComp/codeEditorComp",
+ layoutInfo: {
+ w: 12,
+ h: 50,
+ }
+ },
richTextEditor: {
name: trans("uiComp.richTextEditorCompName"),
enName: "Rich Text Editor",
diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx
index 2395f4f290..35c6f5928e 100644
--- a/client/packages/lowcoder/src/comps/index.tsx
+++ b/client/packages/lowcoder/src/comps/index.tsx
@@ -117,7 +117,8 @@ import {
PieChartCompIcon,
BarChartCompIcon,
LineChartCompIcon,
- ScatterChartCompIcon
+ ScatterChartCompIcon,
+ CodeEditorCompIcon
} from "lowcoder-design";
import { ModuleComp } from "./comps/moduleComp/moduleComp";
import { TableComp } from "./comps/tableComp/tableComp";
@@ -990,6 +991,21 @@ export var uiCompMap: Registry = {
h: 50,
},
},
+ codeEditor: {
+ name: trans("uiComp.codeEditorCompName"),
+ enName: "Code Editor",
+ categories: ["forms"],
+ description: trans("uiComp.codeEditorCompDesc"),
+ icon: CodeEditorCompIcon,
+ keywords: trans("uiComp.codeEditorCompKeywords"),
+ lazyLoad: true,
+ compName: "CodeEditorComp",
+ compPath: "comps/codeEditorComp/codeEditorComp",
+ layoutInfo: {
+ w: 12,
+ h: 50,
+ }
+ },
richTextEditor: {
name: trans("uiComp.richTextEditorCompName"),
enName: "Rich Text Editor",
diff --git a/client/packages/lowcoder/src/comps/uiCompRegistry.ts b/client/packages/lowcoder/src/comps/uiCompRegistry.ts
index 4c320de479..0485dbe2e2 100644
--- a/client/packages/lowcoder/src/comps/uiCompRegistry.ts
+++ b/client/packages/lowcoder/src/comps/uiCompRegistry.ts
@@ -69,6 +69,7 @@ export type UICompType =
| "imageEditor"
| "calendar"
| "password"
+ | "codeEditor"
| "richTextEditor"
| "numberInput"
| "slider"
diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts
index 43bcb39868..06b1b904c5 100644
--- a/client/packages/lowcoder/src/i18n/locales/en.ts
+++ b/client/packages/lowcoder/src/i18n/locales/en.ts
@@ -53,7 +53,70 @@ export const en = {
},
"codeEditor": {
"notSupportAutoFormat": "The current code editor does not support auto-formatting.",
- "fold": "Fold"
+ "fold": "Fold",
+ "properties": {
+ "value": "Default Value",
+ "language": "Language",
+ "theme": "Theme",
+ "lineNumbers": "Line Numbers",
+ "minimap": "Minimap",
+ "stickyScroll": "Sticky Scroll",
+ "lightbulb": "Lightbulb",
+ "hover": "Hover",
+ "folding": "Folding",
+ "readOnly": "Read Only",
+ "resizable": "Resizable"
+ },
+ "methods": {
+ "setValue": "Update the editor's content.",
+ "setLanguage": "Change the programming language.",
+ "setTheme": "Switch the editor theme.",
+ "setLineNumbers": "Control the display of line numbers.",
+ "enableMinimap": "Toggle the minimap display.",
+ "enableStickyScroll": "Toggle sticky scroll.",
+ "enableLightbulb": "Toggles lightbulb feature.",
+ "enableHover": "Enable or disable hover.",
+ "enableFolding": "Enable or disable folding.",
+ "setReadOnly": "Enable or disable read-only mode.",
+ "setResizable": "Enable or disable the ability to resize the editor.",
+ },
+ "theme": {
+ "light": "Light",
+ "dark": "Dark"
+ },
+ "languages": {
+ "yaml": "YAML",
+ "json": "JSON",
+ "xml": "XML",
+ "html": "HTML",
+ "css": "CSS",
+ "ini": "INI",
+ "sql": "SQL",
+ "php": "PHP",
+ "shell": "Shell",
+ "powershell": "PowerShell",
+ "handlebars": "Handlebars",
+ "dockerfile": "Dockerfile",
+ "graphql": "GraphQL",
+ "markdown": "Markdown",
+ "plaintext": "Plaintext",
+ "python": "Python",
+ "ruby": "Ruby",
+ "rust": "Rust",
+ "java": "Java",
+ "c": "C",
+ "csharp": "C#",
+ "cpp": "C++",
+ "go": "Go",
+ "javascript": "JavaScript",
+ "typescript": "TypeScript"
+ },
+ "lineNumberOptions": {
+ "on": "On",
+ "off": "Off",
+ "relative": "Relative",
+ "interval": "Interval"
+ }
},
"exportMethod": {
"setDesc": "Set Property: {property}",
@@ -1014,6 +1077,10 @@ export const en = {
"passwordCompDesc": "A secure field for password input, masking the characters for privacy.",
"passwordCompKeywords": "password, security, input, hidden",
+ "codeEditorCompName": "Code Editor",
+ "codeEditorCompDesc": "A feature-rich code editor component, offering multi-language support, syntax highlighting, and built-in validation.",
+ "codeEditorCompKeywords": "code, editor, programming, syntax highlighting, coding",
+
"richTextEditorCompName": "Rich Text Editor",
"richTextEditorCompDesc": "An advanced text editor supporting rich formatting options like bold, italics, and lists.",
"richTextEditorCompKeywords": "editor, text, formatting, rich content",
diff --git a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx
index a931455d4b..63be02fab0 100644
--- a/client/packages/lowcoder/src/pages/editor/editorConstants.tsx
+++ b/client/packages/lowcoder/src/pages/editor/editorConstants.tsx
@@ -104,6 +104,7 @@ import {
TurnstileCaptchaCompIconSmall,
PivotTableCompIconSmall,
GraphChartCompIconSmall,
+ CodeEditorCompIconSmall,
} from "lowcoder-design";
// Memoize icon components to prevent unnecessary re-renders
@@ -229,6 +230,7 @@ export const CompStateIcon: {
progressCircle: ,
qrCode: ,
rating: ,
+ codeEditor: ,
richTextEditor: ,
scanner: ,
segmentedControl: ,
diff --git a/client/yarn.lock b/client/yarn.lock
index b3885ff806..1d1387e555 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -3030,6 +3030,28 @@ __metadata:
languageName: node
linkType: hard
+"@monaco-editor/loader@npm:^1.5.0":
+ version: 1.5.0
+ resolution: "@monaco-editor/loader@npm:1.5.0"
+ dependencies:
+ state-local: ^1.0.6
+ checksum: 45e5f56ea9b1e5c16e3d40b05f8c365af830627d2aa8215c86cfac57384419c1b896927408c1261a12dc182a08419d4f20a0d0949d3e76ca42ccc68f4ffec508
+ languageName: node
+ linkType: hard
+
+"@monaco-editor/react@npm:^4.7.0":
+ version: 4.7.0
+ resolution: "@monaco-editor/react@npm:4.7.0"
+ dependencies:
+ "@monaco-editor/loader": ^1.5.0
+ peerDependencies:
+ monaco-editor: ">= 0.25.0 < 1"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ checksum: 8b3bd8adfcd6af70dc5f965e986932269e1e2c2a0f6beb5a3c632c8c7942c1341f6086d9664f9a949983bdf4a04a706e529a93bfec3b5884642915dfcc0354c3
+ languageName: node
+ linkType: hard
+
"@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1":
version: 5.1.1-v1
resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1"
@@ -13971,6 +13993,7 @@ coolshapes-react@lowcoder-org/coolshapes-react:
"@babel/preset-env": ^7.20.2
"@babel/preset-typescript": ^7.18.6
"@lottiefiles/react-lottie-player": ^3.5.3
+ "@monaco-editor/react": ^4.7.0
"@remixicon/react": ^4.1.1
"@rollup/plugin-typescript": ^12.1.0
"@supabase/supabase-js": ^2.45.4
@@ -14005,6 +14028,7 @@ coolshapes-react@lowcoder-org/coolshapes-react:
jest-environment-jsdom: ^29.5.0
lint-staged: ^13.0.1
lowcoder-cli: "workspace:^"
+ monaco-editor: ^0.52.2
mq-polyfill: ^1.1.8
number-precision: ^1.6.0
prettier: ^3.1.0
@@ -15585,6 +15609,13 @@ coolshapes-react@lowcoder-org/coolshapes-react:
languageName: node
linkType: hard
+"monaco-editor@npm:^0.52.2":
+ version: 0.52.2
+ resolution: "monaco-editor@npm:0.52.2"
+ checksum: d5ff7b7a469afee25ac708d9ace0dcc5ef24ed328dfc526a52944a497f0d826cfb0685a778ff4b7becc0a8f7843f260c17ea6de3f6719481d53501d79ebb1260
+ languageName: node
+ linkType: hard
+
"moo-color@npm:^1.0.2":
version: 1.0.3
resolution: "moo-color@npm:1.0.3"
@@ -19951,6 +19982,13 @@ coolshapes-react@lowcoder-org/coolshapes-react:
languageName: node
linkType: hard
+"state-local@npm:^1.0.6":
+ version: 1.0.7
+ resolution: "state-local@npm:1.0.7"
+ checksum: d1afcf1429e7e6eb08685b3a94be8797db847369316d4776fd51f3962b15b984dacc7f8e401ad20968e5798c9565b4b377afedf4e4c4d60fe7495e1cbe14a251
+ languageName: node
+ linkType: hard
+
"statuses@npm:2.0.1":
version: 2.0.1
resolution: "statuses@npm:2.0.1"
diff --git a/docs/publish-apps/embedd-an-app/native-embed-sdk/README.md b/docs/publish-apps/embedd-an-app/native-embed-sdk/README.md
index 79dfe6a08f..a2d1161672 100644
--- a/docs/publish-apps/embedd-an-app/native-embed-sdk/README.md
+++ b/docs/publish-apps/embedd-an-app/native-embed-sdk/README.md
@@ -212,6 +212,7 @@ Components cover a wide range of functionalities, from input and selection to di
* **QRCodeComp** - Component for displaying QR codes.
* **RadioComp** - Component for radio button functionality, allowing users to select a single option from a set.
* **RatingComp** - Component for rating functionality, allowing users to provide a rating, typically represented by stars or similar indicators.
+* **CodeEditorComp** - Component for code editing, offering multi-language support, syntax highlighting, and built-in validation.
* **RichTextEditorComp** - Component for rich text editing, providing a user interface for formatting text with options like bold, italic, lists, etc.
* **SliderComp** - Component for slider functionality, allowing users to select a value from a range by sliding a handle.
* **SwitchComp** - Component for switch functionality, allowing users to toggle between two states, such as on/off.
From 03db9f774eb04a1b9ed911af636e2e148b990d26 Mon Sep 17 00:00:00 2001
From: Nangelov7 <25034472+Nangelov7@users.noreply.github.com>
Date: Tue, 8 Jul 2025 18:35:27 +0300
Subject: [PATCH 2/2] feat(code-editor): enable label, tooltip, required field,
and word wrap support
---
.../comps/codeEditorComp/codeEditorComp.tsx | 90 ++++++++++++++++++-
.../comps/controls/styleControlConstants.tsx | 4 +
.../packages/lowcoder/src/i18n/locales/en.ts | 11 ++-
3 files changed, 99 insertions(+), 6 deletions(-)
diff --git a/client/packages/lowcoder/src/comps/comps/codeEditorComp/codeEditorComp.tsx b/client/packages/lowcoder/src/comps/comps/codeEditorComp/codeEditorComp.tsx
index 0a9b731b05..ab26c326f8 100644
--- a/client/packages/lowcoder/src/comps/comps/codeEditorComp/codeEditorComp.tsx
+++ b/client/packages/lowcoder/src/comps/comps/codeEditorComp/codeEditorComp.tsx
@@ -8,9 +8,12 @@ import {
eventHandlerControl,
stringExposingStateControl,
BoolControl,
+ LabelControl,
+ styleControl,
dropdownControl,
AutoHeightControl,
} from "lowcoder-sdk";
+import { CodeEditorContainerStyle, LabelStyle } from "comps/controls/styleControlConstants";
import { useResizeDetector } from "react-resize-detector";
import Editor from "@monaco-editor/react";
import { styled } from "styled-components";
@@ -18,6 +21,7 @@ import { trans } from "i18n";
import { useRef, useCallback, useLayoutEffect } from "react";
import debounce from "lodash/debounce";
import * as monacoEditor from "monaco-editor";
+import { formDataChildren, FormDataPropertyView } from "../../comps/formComp/formDataConstants";
const CodeEditorWrapper = styled.div`
border: 1px solid #dddddd;
@@ -58,6 +62,7 @@ let CodeEditorTmpComp = (function () {
language: "yaml",
theme: "light",
lineNumbers: "on",
+ wordWrap: "on",
lightbulb: monacoEditor.editor.ShowLightbulbIconMode.OnCode,
enabled: true,
disabled: false,
@@ -76,11 +81,19 @@ let CodeEditorTmpComp = (function () {
{ label: trans("codeEditor.lineNumberOptions.relative"), value: "relative" },
].sort((a, b) => a.label.localeCompare(b.label))
+ const wordWrapOptions = [
+ { label: trans("codeEditor.wordWrapOptions.on"), value: "on" },
+ { label: trans("codeEditor.wordWrapOptions.off"), value: "off" },
+ { label: trans("codeEditor.wordWrapOptions.wordWrapColumn"), value: "wordWrapColumn" },
+ { label: trans("codeEditor.wordWrapOptions.bounded"), value: "bounded" },
+ ].sort((a, b) => a.label.localeCompare(b.label))
+
const childrenMap = {
autoHeight: withDefault(AutoHeightControl, "auto"),
language: dropdownControl(languages, defaultValues.language),
theme: dropdownControl(themes, defaultValues.theme),
lineNumbers: dropdownControl(lineNumbersOptions, defaultValues.lineNumbers),
+ wordWrap: dropdownControl(wordWrapOptions, defaultValues.wordWrap),
minimap: withDefault(BoolControl, defaultValues.enabled),
stickyScroll: withDefault(BoolControl, defaultValues.enabled),
lightbulb: withDefault(BoolControl, defaultValues.enabled),
@@ -88,6 +101,17 @@ let CodeEditorTmpComp = (function () {
folding: withDefault(BoolControl, defaultValues.enabled),
readOnly: withDefault(BoolControl, defaultValues.disabled),
value: stringExposingStateControl("text", defaultValues.value),
+ required: withDefault(BoolControl, defaultValues.disabled),
+ label: withDefault(LabelControl, {
+ text: "Code Editor",
+ tooltip: "",
+ hidden: false,
+ widthUnit: "%",
+ position: "column",
+ align: "left"
+ }),
+ style: styleControl(CodeEditorContainerStyle , "style"),
+ labelStyle: styleControl(LabelStyle , 'labelStyle'),
onEvent: eventHandlerControl([
{
label: "onChange",
@@ -95,6 +119,7 @@ let CodeEditorTmpComp = (function () {
description: "Triggers when data changes",
},
] as const),
+ ...formDataChildren,
};
return new UICompBuilder(childrenMap, (props) => {
@@ -174,7 +199,10 @@ let CodeEditorTmpComp = (function () {
}
}, [props.value.value]);
- return (
+ return props.label({
+ required: props.required,
+ style: props.style,
+ children: (
- );
+ )
+ })
})
.setPropertyViewFn((children: any) => {
return (
@@ -229,19 +259,33 @@ let CodeEditorTmpComp = (function () {
{children.language.propertyView({ label: trans("codeEditor.properties.language") })}
{children.theme.propertyView({ label: trans("codeEditor.properties.theme") })}
{children.lineNumbers.propertyView({ label: trans("codeEditor.properties.lineNumbers") })}
+ {children.wordWrap.propertyView({ label: trans("codeEditor.properties.wordWrap") })}
{children.minimap.propertyView({ label: trans("codeEditor.properties.minimap") })}
{children.stickyScroll.propertyView({ label: trans("codeEditor.properties.stickyScroll")})}
{children.lightbulb.propertyView({ label: trans("codeEditor.properties.lightbulb") })}
{children.hover.propertyView({ label: trans("codeEditor.properties.hover") })}
{children.folding.propertyView({ label: trans("codeEditor.properties.folding") })}
- {children.readOnly.propertyView({ label: trans("codeEditor.properties.readOnly") })}
+ {children.label.getPropertyView()}
{children.onEvent.propertyView()}
-
+
{children.autoHeight.getPropertyView()}
+
+ {children.readOnly.propertyView({ label: trans("codeEditor.properties.readOnly") })}
+
+
+ {children.required.propertyView({ label: trans("codeEditor.properties.required") })}
+
+
+ {children.style.getPropertyView()}
+
+
+ {children.labelStyle.getPropertyView()}
+
+
>
);
})
@@ -427,6 +471,24 @@ CodeEditorTmpComp = withMethodExposing(CodeEditorTmpComp, [
}
}
},
+ {
+ method: {
+ name: "enableWordWrap",
+ description: trans("codeEditor.methods.enableWordWrap"),
+ params: [{
+ name: "wordWrap",
+ type: "boolean",
+ description: "boolean"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.wordWrap.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.wordWrap.dispatchChangeValueAction(values);
+ }
+ }
+ },
{
method: {
name: "setReadOnly",
@@ -445,6 +507,24 @@ CodeEditorTmpComp = withMethodExposing(CodeEditorTmpComp, [
}
}
},
+ {
+ method: {
+ name: "markAsRequired",
+ description: trans("codeEditor.methods.markAsRequired"),
+ params: [{
+ name: "required",
+ type: "boolean",
+ description: "boolean"
+ }],
+ },
+ execute: (comp: any, values: any[]) => {
+ if(Array.isArray(values)) {
+ comp.children.required.dispatchChangeValueAction(values[0]);
+ } else {
+ comp.children.required.dispatchChangeValueAction(values);
+ }
+ }
+ },
]);
export const CodeEditorComp = withExposingConfigs(CodeEditorTmpComp, [
@@ -452,10 +532,12 @@ export const CodeEditorComp = withExposingConfigs(CodeEditorTmpComp, [
new NameConfig("language", trans("codeEditor.properties.language")),
new NameConfig("theme", trans("codeEditor.properties.theme")),
new NameConfig("lineNumbers", trans("codeEditor.properties.lineNumbers")),
+ new NameConfig("wordWrap", trans("codeEditor.properties.wordWrap")),
new NameConfig("minimap", trans("codeEditor.properties.minimap")),
new NameConfig("stickyScroll", trans("codeEditor.properties.stickyScroll")),
new NameConfig("lightbulb", trans("codeEditor.properties.lightbulb")),
new NameConfig("hover", trans("codeEditor.properties.hover")),
new NameConfig("folding", trans("codeEditor.properties.folding")),
new NameConfig("readOnly", trans("codeEditor.properties.readOnly")),
+ new NameConfig("required", trans("codeEditor.properties.required")),
]);
\ No newline at end of file
diff --git a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
index 176afbbfc9..011c5a8552 100644
--- a/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
+++ b/client/packages/lowcoder/src/comps/controls/styleControlConstants.tsx
@@ -1446,6 +1446,10 @@ export const SignatureContainerStyle = [
// ...STYLING_FIELDS_CONTAINER_SEQUENCE,
] as const;
+export const CodeEditorContainerStyle = [
+ getStaticBorder(),
+] as const;
+
export const RatingStyle = [
{
name: "checked",
diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts
index 06b1b904c5..36807303a7 100644
--- a/client/packages/lowcoder/src/i18n/locales/en.ts
+++ b/client/packages/lowcoder/src/i18n/locales/en.ts
@@ -64,8 +64,9 @@ export const en = {
"lightbulb": "Lightbulb",
"hover": "Hover",
"folding": "Folding",
+ "wordWrap": "Word Wrap",
"readOnly": "Read Only",
- "resizable": "Resizable"
+ "required": "Required",
},
"methods": {
"setValue": "Update the editor's content.",
@@ -78,7 +79,7 @@ export const en = {
"enableHover": "Enable or disable hover.",
"enableFolding": "Enable or disable folding.",
"setReadOnly": "Enable or disable read-only mode.",
- "setResizable": "Enable or disable the ability to resize the editor.",
+ "markAsRequired": "Marks the field as required, preventing submission if left empty.",
},
"theme": {
"light": "Light",
@@ -116,6 +117,12 @@ export const en = {
"off": "Off",
"relative": "Relative",
"interval": "Interval"
+ },
+ "wordWrapOptions": {
+ "on": "On",
+ "off": "Off",
+ "wordWrapColumn": "Column",
+ "bounded": "Bounded"
}
},
"exportMethod": {