Skip to content

Commit b38a416

Browse files
feat(agentflow): add Rich Text editor for Content Editing (FLOWISE-342) (#5988)
* Add TipTap RichTextEditor atom for expandable content editing Introduce a RichTextEditor atom wrapping TipTap with code block syntax highlighting (lowlight). Update ExpandTextDialog to support a 'richtext' mode and replace plain TextFields in MessagesInput with the new editor. Lazy-load RichTextEditor via React.lazy + Suspense to keep the initial bundle lean. Import only 4 highlight.js languages (js/json/py/ts) instead of lowlight/common (~400KB savings). FLOWISE-342 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix CI lockfile mismatch and use TipTap native autofocus Remove stale @types/uuid from pnpm-lock.yaml to fix frozen-lockfile CI failure. Replace setTimeout-based autofocus with TipTap's built-in autofocus option for more reliable focus behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix React warnings in MessagesInput and RichTextEditor - Generate item keys synchronously to fix missing key prop warning - Wrap IconVariable in span for MUI Tooltip ref forwarding - Use :first-of-type/:last-of-type instead of :first-child/:last-child Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix syntax highlighting and block element spacing in RichTextEditor Add CSS selectors for hljs classes so lowlight syntax colors are actually applied. Fix block spacing by targeting only the first/last child of the editor instead of first/last-of-type per element. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * address gemini review comment, fix css warning on ssr * fix rich text editor lazy load flow * address gemini review feedback * address review feedback --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8d9ec99 commit b38a416

20 files changed

Lines changed: 695 additions & 117 deletions

packages/agentflow/.eslintrc.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ module.exports = {
8181
'import/first': 'error',
8282
'import/newline-after-import': 'error',
8383
'import/no-duplicates': 'error',
84+
// Allow autoFocus on custom components (e.g. RichTextEditor in dialogs) — they manage
85+
// focus programmatically per WAI-ARIA dialog patterns. Native elements are still flagged.
86+
'jsx-a11y/no-autofocus': ['error', { ignoreNonDOM: true }],
8487
'prettier/prettier': 'error',
8588
// Ban @/features alias — features use relative imports internally, and no other
8689
// layer should import from features (enforced by import/no-restricted-paths below).
@@ -116,8 +119,8 @@ module.exports = {
116119
{
117120
target: './src/atoms',
118121
from: './src/core',
119-
except: ['./types'],
120-
message: 'Atoms can only import types from core/types, not utilities or business logic.'
122+
except: ['./types', './theme'],
123+
message: 'Atoms can only import from core/types and core/theme, not utilities or business logic.'
121124
},
122125
// core/ cannot import from anything (leaf node)
123126
{

packages/agentflow/ARCHITECTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ atoms/
4242
- No API calls
4343
- Stateless or minimal local state
4444
- Imported by features, never the reverse
45-
- **Forbidden**: Importing from `features/` or `infrastructure/` (except types from `core/types` for prop definitions)
45+
- **Forbidden**: Importing from `features/` or `infrastructure/` (except types from `core/types` for prop definitions and design tokens from `core/theme`)
4646

4747
**Goal:** 100% visual consistency.
4848

@@ -210,7 +210,7 @@ infrastructure/
210210

211211
- `features``atoms`, `infrastructure`, `core`
212212
- `infrastructure``core`
213-
- `atoms``core/types` only (for type definitions) ✅
213+
- `atoms``core/types` and `core/theme` only (for type definitions and design tokens) ✅
214214
- `core` → nothing (leaf node) ✅
215215
- **Atoms and Core are "leaf" nodes** - they cannot import from `features/` or `infrastructure/`
216216

packages/agentflow/examples/src/demos/BasicExample.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,31 @@ const initialFlow: FlowData = {
2828
hideInput: true,
2929
outputAnchors: [{ id: 'startAgentflow_0-output-0', name: 'start', label: 'Start', type: 'start' }]
3030
}
31+
},
32+
{
33+
id: 'agentAgentflow_0',
34+
type: 'agentflowNode',
35+
position: { x: 250, y: 100 },
36+
data: {
37+
id: 'agentAgentflow_0',
38+
name: 'agentAgentflow',
39+
label: 'Agent',
40+
color: '#4DD0E1',
41+
outputAnchors: [{ id: 'agentAgentflow_0-output-0', name: 'output', label: 'Output', type: 'string' }]
42+
}
43+
}
44+
],
45+
edges: [
46+
{
47+
id: 'edge-1',
48+
source: 'startAgentflow_0',
49+
sourceHandle: 'startAgentflow_0-output-0',
50+
target: 'agentAgentflow_0',
51+
targetHandle: 'agentAgentflow_0',
52+
type: 'agentflowEdge',
53+
data: { sourceColor: '#7EE787', targetColor: '#4DD0E1' }
3154
}
3255
],
33-
edges: [],
3456
viewport: { x: 0, y: 0, zoom: 1 }
3557
}
3658

packages/agentflow/jest.config.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@ const baseConfig = {
1212
},
1313
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
1414
moduleNameMapper: {
15-
'^@test-utils/(.*)$': '<rootDir>/src/__test_utils__/$1',
15+
'\\.(css|less|scss|sass)$': '<rootDir>/src/__mocks__/styleMock.js',
1616
'\\.svg$': '<rootDir>/src/__mocks__/styleMock.js',
1717
'^@/(.*)$': '<rootDir>/src/$1',
18-
'\\.(css|less|scss|sass)$': '<rootDir>/src/__mocks__/styleMock.js'
18+
'^@test-utils/(.*)$': '<rootDir>/src/__test_utils__/$1',
19+
// TipTap + lowlight ship ESM-only — Jest (CJS) cannot import them,
20+
// so we redirect to lightweight CJS stubs under src/__mocks__/.
21+
'^@tiptap/(.+)$': '<rootDir>/src/__mocks__/@tiptap/$1.ts',
22+
'^lowlight$': '<rootDir>/src/__mocks__/lowlight.ts',
23+
// Bypass React.lazy wrappers — resolve Foo.lazy → Foo so tests render synchronously
24+
'(.*)\\.lazy$': '$1'
1925
}
2026
}
2127

@@ -39,6 +45,10 @@ module.exports = {
3945
'./src/*.ts': { branches: 80, functions: 80, lines: 80, statements: 80 },
4046
'./src/Agentflow.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
4147
'./src/atoms/ArrayInput.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
48+
'./src/atoms/ExpandTextDialog.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
49+
'./src/atoms/MessagesInput.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
50+
// Tier 3 UI atom — only the onChange/disabled/sync logic is tested, not styled internals
51+
'./src/atoms/RichTextEditor.tsx': { branches: 30, functions: 50, lines: 50, statements: 50 },
4252
'./src/core/': { branches: 80, functions: 80, lines: 80, statements: 80 },
4353
'./src/features/canvas/components/ConnectionLine.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
4454
// Only getMinimumNodeHeight() is tested; the component is Tier 3 UI with no business logic

packages/agentflow/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,13 @@
6969
},
7070
"dependencies": {
7171
"@tabler/icons-react": "^3.7.0",
72+
"@tiptap/extension-code-block-lowlight": "^3.4.3",
73+
"@tiptap/extension-placeholder": "^2.11.5",
74+
"@tiptap/react": "^2.11.5",
75+
"@tiptap/starter-kit": "^2.11.5",
7276
"axios": "^1.7.2",
7377
"lodash": "^4.17.21",
78+
"lowlight": "^3.3.0",
7479
"uuid": "^10.0.0"
7580
},
7681
"devDependencies": {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const CodeBlockLowlight = {
2+
configure: jest.fn(() => 'CodeBlockLowlight'),
3+
extend: jest.fn(() => 'CodeBlockLowlightExtended')
4+
}
5+
export default CodeBlockLowlight
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const Placeholder = { configure: jest.fn(() => 'Placeholder') }
2+
export default Placeholder
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createElement, forwardRef } from 'react'
2+
3+
export const useEditor = (config?: Record<string, unknown>) => ({
4+
getHTML: () => (config?.content as string) ?? '<p></p>',
5+
setEditable: jest.fn(),
6+
commands: { focus: jest.fn(), setContent: jest.fn() },
7+
_onUpdate: config?.onUpdate
8+
})
9+
10+
export const EditorContent = forwardRef<HTMLDivElement, { editor?: unknown; [k: string]: unknown }>(({ editor, ...rest }, ref) =>
11+
createElement('div', { ref, 'data-testid': 'tiptap-editor-content', 'data-has-editor': !!editor, ...rest })
12+
)
13+
EditorContent.displayName = 'EditorContent'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const StarterKit = { configure: jest.fn(() => 'StarterKit') }
2+
export default StarterKit
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const common = {}
2+
export const createLowlight = jest.fn(() => ({ register: jest.fn() }))

0 commit comments

Comments
 (0)