mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 12:53:32 +00:00
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:
@@ -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",
|
||||
|
||||
+93
@@ -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;
|
||||
+2
-1
@@ -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>[] {
|
||||
|
||||
Reference in New Issue
Block a user