diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx index fb76b0fc6..41aee7197 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFormServerlessFunction.tsx @@ -8,6 +8,7 @@ import { WorkflowCodeAction } from '@/workflow/types/Workflow'; import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; import { setNestedValue } from '@/workflow/workflow-steps/workflow-actions/utils/setNestedValue'; +import { Monaco } from '@monaco-editor/react'; import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton'; import { ServerlessFunctionExecutionResult } from '@/serverless-functions/components/ServerlessFunctionExecutionResult'; import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath'; @@ -26,13 +27,13 @@ import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/w import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { Monaco } from '@monaco-editor/react'; import { editor } from 'monaco-editor'; import { AutoTypings } from 'monaco-editor-auto-typings'; import { useEffect, useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { CodeEditor, IconCode, IconPlayerPlay, isDefined } from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; +import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers'; const StyledContainer = styled.div` display: flex; @@ -299,6 +300,7 @@ export const WorkflowEditActionFormServerlessFunction = ({ language={'typescript'} onChange={handleCodeChange} onMount={handleEditorDidMount} + setMarkers={getWrongExportedFunctionMarkers} options={{ readOnly: actionOptions.readonly, domReadOnly: actionOptions.readonly, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/getWrongExportedFunctionMarkers.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/getWrongExportedFunctionMarkers.test.ts new file mode 100644 index 000000000..a93a4b725 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/getWrongExportedFunctionMarkers.test.ts @@ -0,0 +1,30 @@ +import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers'; + +describe('getWrongExportedFunctionMarkers', () => { + it('should return marker when no exported function', () => { + const value = 'const main = async () => {}'; + const result = getWrongExportedFunctionMarkers(value); + expect(result.length).toEqual(1); + expect(result[0].message).toEqual( + 'An exported "main" arrow function is required.', + ); + }); + + it('should return marker when no wrong exported function', () => { + const value = 'export const wrongMain = async () => {}'; + const result = getWrongExportedFunctionMarkers(value); + expect(result.length).toEqual(1); + }); + + it('should return no marker when valid exported function', () => { + const value = 'export const main = async () => {}'; + const result = getWrongExportedFunctionMarkers(value); + expect(result.length).toEqual(0); + }); + + it('should return handle multiple spaces', () => { + const value = 'export const main = async () => {}'; + const result = getWrongExportedFunctionMarkers(value); + expect(result.length).toEqual(0); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers.ts new file mode 100644 index 000000000..525914104 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/getWrongExportedFunctionMarkers.ts @@ -0,0 +1,60 @@ +import { isDefined } from 'twenty-ui'; + +const getSubstringCoordinate = ( + text: string, + substring: string, +): { line: number; column: number } | null => { + const lines = text.split('\n'); + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const columnIndex = lines[lineIndex].indexOf(substring); + if (columnIndex !== -1) { + return { + line: lineIndex + 1, // 1-based line number + column: columnIndex + 1, // 1-based column number + }; + } + } + + return null; +}; + +export const getWrongExportedFunctionMarkers = (value: string) => { + const validRegex = /export\s+const\s+main\s*=/g; + const invalidRegex = /export\s+const\s+\S*/g; + const exportRegex = /export\s+const/g; + const validMatch = value.match(validRegex); + const invalidMatch = value.match(invalidRegex); + const exportMatch = value.match(exportRegex); + const markers = []; + + if (!validMatch && !!invalidMatch) { + const coordinates = getSubstringCoordinate(value, invalidMatch[0]); + if (isDefined(coordinates)) { + const endColumn = invalidMatch[0].length + coordinates.column; + markers.push({ + severity: 8, //MarkerSeverity.Error, + message: 'Exported arrow function should be named "main"', + code: 'export const main', + startLineNumber: coordinates.line, + startColumn: coordinates.column, + endLineNumber: 1, + endColumn, + }); + } + } + + if (!exportMatch) { + markers.push({ + severity: 8, //MarkerSeverity.Error, + message: 'An exported "main" arrow function is required.', + code: 'export const main', + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: 1, + }); + } + + return markers; +}; diff --git a/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx b/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx index ab1b8c99c..a671a4434 100644 --- a/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx +++ b/packages/twenty-ui/src/input/code-editor/components/CodeEditor.tsx @@ -1,11 +1,14 @@ import { useTheme, css } from '@emotion/react'; -import Editor, { EditorProps } from '@monaco-editor/react'; +import Editor, { EditorProps, Monaco } from '@monaco-editor/react'; import { codeEditorTheme } from '@ui/input'; import { isDefined } from '@ui/utilities'; import styled from '@emotion/styled'; +import { useState } from 'react'; +import { editor } from 'monaco-editor'; type CodeEditorProps = Omit & { onChange?: (value: string) => void; + setMarkers?: (value: string) => editor.IMarkerData[]; withHeader?: boolean; }; @@ -35,12 +38,31 @@ export const CodeEditor = ({ language, onMount, onChange, + setMarkers, onValidate, height = 450, withHeader = false, options, }: CodeEditorProps) => { const theme = useTheme(); + const [monaco, setMonaco] = useState(undefined); + const [editor, setEditor] = useState< + editor.IStandaloneCodeEditor | undefined + >(undefined); + + const setModelMarkers = ( + editor: editor.IStandaloneCodeEditor | undefined, + monaco: Monaco | undefined, + ) => { + const model = editor?.getModel(); + if (!isDefined(model)) { + return; + } + const customMarkers = setMarkers?.(model.getValue()); + if (isDefined(customMarkers)) { + monaco?.editor.setModelMarkers(model, 'customMarker', customMarkers); + } + }; return ( { + setMonaco(monaco); + setEditor(editor); monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme)); monaco.editor.setTheme('codeEditorTheme'); - onMount?.(editor, monaco); + setModelMarkers(editor, monaco); }} onChange={(value) => { if (isDefined(value)) { onChange?.(value); + setModelMarkers(editor, monaco); } }} - onValidate={onValidate} + onValidate={(markers) => { + onValidate?.(markers); + }} options={{ overviewRulerLanes: 0, scrollbar: {