feat: Integrate Lexical editor for custom fields in cuesheet

- Added lexical and @lexical/react dependencies.
- Created LexicalEditorCell component to provide an on-click editable rich text field.
- Modified MakeCustomField in cuesheetColsFactory to use LexicalEditorCell for string-type custom fields.
- Added Playwright tests for the new Lexical editor functionality in the cuesheet.

Note: Playwright tests could not be fully run due to persistent webServer timeouts, but test code is included.
This commit is contained in:
google-labs-jules[bot]
2025-07-11 13:18:40 +00:00
parent e963e183db
commit 67ebd12a94
6 changed files with 404 additions and 5 deletions
+3 -1
View File
@@ -34,7 +34,9 @@
"react-router-dom": "^6.3.0",
"react-simple-code-editor": "^0.14.1",
"web-vitals": "^3.1.1",
"zustand": "^5.0.3"
"zustand": "^5.0.3",
"lexical": "^0.17.0",
"@lexical/react": "^0.17.0"
},
"scripts": {
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
@@ -0,0 +1,93 @@
import {
$getRoot,
$getSelection,
EditorState,
LexicalEditor,
} from 'lexical';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import React, { useState, useCallback } from 'react';
interface LexicalEditorCellProps {
initialValue: string;
onSave: (value: string) => void;
}
const editorTheme = {
// Minimal theme, can be expanded later
ltr: 'ltr',
rtl: 'rtl',
placeholder: 'editor-placeholder',
paragraph: 'editor-paragraph',
};
function LexicalEditorCell({ initialValue, onSave }: LexicalEditorCellProps) {
const [isEditing, setIsEditing] = useState(false);
const [currentValue, setCurrentValue] = useState(initialValue);
const [editorState, setEditorState] = useState<EditorState | null>(null);
const initialConfig = {
namespace: 'LexicalEditorCell',
theme: editorTheme,
onError: (error: Error) => {
console.error(error);
// Optionally, you could add more robust error handling here,
// like notifying the user or attempting to recover.
},
editorState: null, // No initial editor state when not editing
};
const handleCellClick = useCallback(() => {
setIsEditing(true);
}, []);
const handleBlur = useCallback(() => {
setIsEditing(false);
if (editorState) {
editorState.read(() => {
const root = $getRoot();
const selection = $getSelection();
// For simplicity, we'll just get the text content.
// For actual rich text, you'd want to serialize to JSON.
const newTextValue = root.getTextContent();
onSave(newTextValue);
setCurrentValue(newTextValue); // Update local display value
});
}
}, [onSave, editorState]);
const onChange = (newEditorState: EditorState, editor: LexicalEditor) => {
setEditorState(newEditorState);
};
if (!isEditing) {
return (
<div onClick={handleCellClick} style={{ cursor: 'pointer', minHeight: '20px', padding: '5px' }}>
{currentValue || <span style={{color: '#aaa'}}>Click to edit...</span>}
</div>
);
}
return (
<LexicalComposer initialConfig={{...initialConfig, editorState: currentValue ? undefined : null}}>
<RichTextPlugin
contentEditable={<ContentEditable style={{minHeight: '150px', resize:'vertical', overflow:'auto', border:'1px solid #ccc', padding: '5px'}} />}
placeholder={<div style={{color: '#aaa', position: 'absolute', top: '30px', left: '10px'}}>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<OnChangePlugin onChange={onChange} />
{/* We need a way to trigger save, onBlur for the ContentEditable is one way */}
{/* Attaching onBlur directly to ContentEditable or its parent div within Lexical structure */}
<div onBlur={handleBlur} tabIndex={-1}> {/* Wrapper to capture blur */}
{/* This div is part of the Lexical structure and will be replaced by ContentEditable */}
</div>
</LexicalComposer>
);
}
export default LexicalEditorCell;
@@ -8,6 +8,7 @@ import { formatDuration, formatTime } from '../../../../common/utils/time';
import DurationInput from './DurationInput';
import EditableImage from './EditableImage';
import LexicalEditorCell from './LexicalEditorCell';
import MultiLineCell from './MultiLineCell';
import MutedText from './MutedText';
import SingleLineCell from './SingleLineCell';
@@ -170,7 +171,7 @@ function MakeCustomField({ row, column, table }: CellContext<OntimeEntry, unknow
// fields will not contain the field if there is no value set by the user
// event if there is no initial value, we still render the cell
const initialValue = event.custom[column.id] ?? '';
return <MultiLineCell initialValue={initialValue} handleUpdate={update} />;
return <LexicalEditorCell initialValue={initialValue} onSave={update} />;
}
export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<OntimeEntry>[] {