mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 10:53:51 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f80e957036 |
@@ -12,6 +12,8 @@
|
|||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.6",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@fontsource/open-sans": "^5.0.28",
|
"@fontsource/open-sans": "^5.0.28",
|
||||||
|
"@lexical/react": "^0.16.1",
|
||||||
|
"@lexical/rich-text": "^0.16.1",
|
||||||
"@mantine/hooks": "^7.17.2",
|
"@mantine/hooks": "^7.17.2",
|
||||||
"@sentry/react": "^8.43.0",
|
"@sentry/react": "^8.43.0",
|
||||||
"@table-nav/react": "^0.0.7",
|
"@table-nav/react": "^0.0.7",
|
||||||
@@ -34,7 +36,8 @@
|
|||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-simple-code-editor": "^0.14.1",
|
"react-simple-code-editor": "^0.14.1",
|
||||||
"web-vitals": "^3.1.1",
|
"web-vitals": "^3.1.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3",
|
||||||
|
"lexical": "^0.16.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
"addversion": "node -p \"'export const ONTIME_VERSION = ' + JSON.stringify(require('../../package.json').version) + ';'\" > src/ONTIME_VERSION.js",
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
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 { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
||||||
|
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
||||||
|
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
||||||
|
import { TRANSFORMERS } from '@lexical/markdown';
|
||||||
|
import { $getRoot, $getSelection, EditorState, FORMAT_TEXT_COMMAND, LexicalEditor, $isRangeSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW, $createParagraphNode, $createTextNode, $patchStyleText } from 'lexical';
|
||||||
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { mergeRegister } from '@lexical/utils';
|
||||||
|
import { LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
||||||
|
import { ListItemNode, ListNode } from '@lexical/list';
|
||||||
|
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
||||||
|
import { CodeNode, CodeHighlightNode } from '@lexical/code';
|
||||||
|
import { TableNode, TableCellNode, TableRowNode } from '@lexical/table';
|
||||||
|
|
||||||
|
// Define initial editor configuration
|
||||||
|
const editorConfig = {
|
||||||
|
namespace: 'MyEditor',
|
||||||
|
theme: {
|
||||||
|
ltr: 'ltr',
|
||||||
|
rtl: 'rtl',
|
||||||
|
placeholder: 'editor-placeholder',
|
||||||
|
paragraph: 'editor-paragraph',
|
||||||
|
quote: 'editor-quote',
|
||||||
|
heading: {
|
||||||
|
h1: 'editor-heading-h1',
|
||||||
|
h2: 'editor-heading-h2',
|
||||||
|
h3: 'editor-heading-h3',
|
||||||
|
h4: 'editor-heading-h4',
|
||||||
|
h5: 'editor-heading-h5',
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
nested: {
|
||||||
|
listitem: 'editor-nested-listitem',
|
||||||
|
},
|
||||||
|
ol: 'editor-list-ol',
|
||||||
|
ul: 'editor-list-ul',
|
||||||
|
listitem: 'editor-listitem',
|
||||||
|
},
|
||||||
|
link: 'editor-link',
|
||||||
|
text: {
|
||||||
|
bold: 'editor-text-bold',
|
||||||
|
italic: 'editor-text-italic',
|
||||||
|
underline: 'editor-text-underline',
|
||||||
|
strikethrough: 'editor-text-strikethrough',
|
||||||
|
underlineStrikethrough: 'editor-text-underlineStrikethrough',
|
||||||
|
code: 'editor-text-code',
|
||||||
|
},
|
||||||
|
code: 'editor-code',
|
||||||
|
codeHighlight: {
|
||||||
|
atrule: 'editor-tokenAttr',
|
||||||
|
attr: 'editor-tokenAttr',
|
||||||
|
boolean: 'editor-tokenProperty',
|
||||||
|
builtin: 'editor-tokenSelector',
|
||||||
|
cdata: 'editor-tokenComment',
|
||||||
|
char: 'editor-tokenSelector',
|
||||||
|
clike: 'editor-tokenComment',
|
||||||
|
comment: 'editor-tokenComment',
|
||||||
|
contentType: 'editor-tokenComment',
|
||||||
|
coord: 'editor-tokenComment',
|
||||||
|
deleted: 'editor-tokenProperty',
|
||||||
|
doctype: 'editor-tokenComment',
|
||||||
|
doi: 'editor-tokenComment',
|
||||||
|
entity: 'editor-tokenOperator',
|
||||||
|
function: 'editor-tokenFunction',
|
||||||
|
important: 'editor-tokenVariable',
|
||||||
|
inserted: 'editor-tokenSelector',
|
||||||
|
keyword: 'editor-tokenAttr',
|
||||||
|
markup: 'editor-tokenComment',
|
||||||
|
merged: 'editor-tokenComment',
|
||||||
|
namespace: 'editor-tokenVariable',
|
||||||
|
number: 'editor-tokenProperty',
|
||||||
|
operator: 'editor-tokenOperator',
|
||||||
|
prolog: 'editor-tokenComment',
|
||||||
|
property: 'editor-tokenProperty',
|
||||||
|
punctuation: 'editor-tokenPunctuation',
|
||||||
|
regex: 'editor-tokenVariable',
|
||||||
|
selector: 'editor-tokenSelector',
|
||||||
|
string: 'editor-tokenSelector',
|
||||||
|
style: 'editor-tokenComment',
|
||||||
|
symbol: 'editor-tokenProperty',
|
||||||
|
tag: 'editor-tokenProperty',
|
||||||
|
url: 'editor-tokenOperator',
|
||||||
|
variable: 'editor-tokenVariable',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onError(error: Error) {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
HeadingNode,
|
||||||
|
ListNode,
|
||||||
|
ListItemNode,
|
||||||
|
QuoteNode,
|
||||||
|
CodeNode,
|
||||||
|
CodeHighlightNode,
|
||||||
|
TableNode,
|
||||||
|
TableCellNode,
|
||||||
|
TableRowNode,
|
||||||
|
LinkNode,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RichTextEditorProps {
|
||||||
|
initialValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RichTextEditor({ initialValue, onChange }: RichTextEditorProps) {
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const editorRef = useRef<LexicalEditor | null>(null); // Use ref for editor instance
|
||||||
|
const editorWrapperRef = useRef<HTMLDivElement>(null); // Ref for the entire editor + toolbar wrapper
|
||||||
|
|
||||||
|
const handleEditorFocus = () => {
|
||||||
|
setIsFocused(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// This handleBlur is for the wrapper around the editor and toolbar
|
||||||
|
const handleWrapperBlur = (event: React.FocusEvent<HTMLDivElement>) => {
|
||||||
|
// Check if the new focused element is still within the editor wrapper
|
||||||
|
if (editorWrapperRef.current && !editorWrapperRef.current.contains(event.relatedTarget as Node)) {
|
||||||
|
setIsFocused(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = useCallback((editorState: EditorState) => {
|
||||||
|
if (onChange) {
|
||||||
|
editorState.read(() => {
|
||||||
|
const root = $getRoot();
|
||||||
|
// For now, sending plain text. Could be HTML or Lexical's format.
|
||||||
|
onChange(root.getTextContent());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
// Placeholder for initial value loading
|
||||||
|
useEffect(() => {
|
||||||
|
const currentEditor = editorRef.current;
|
||||||
|
if (currentEditor && initialValue && isFocused) { // only update if focused and has initial value
|
||||||
|
currentEditor.update(() => {
|
||||||
|
const root = $getRoot();
|
||||||
|
root.clear();
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
paragraph.append($createTextNode(initialValue));
|
||||||
|
root.append(paragraph);
|
||||||
|
root.selectEnd(); // Move cursor to the end
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [initialValue, isFocused]); // Rerun when isFocused changes or initialValue changes (though initialValue should be stable)
|
||||||
|
|
||||||
|
|
||||||
|
if (!isFocused) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={handleEditorFocus}
|
||||||
|
onFocus={handleEditorFocus}
|
||||||
|
tabIndex={0}
|
||||||
|
style={{ border: '1px solid #ccc', padding: '10px', minHeight: '50px', cursor: 'text' }}
|
||||||
|
role="textbox" // for accessibility
|
||||||
|
aria-placeholder="Click to edit..."
|
||||||
|
>
|
||||||
|
{initialValue || 'Click to edit...'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare initial config for when the editor mounts
|
||||||
|
const localEditorConfig = {
|
||||||
|
...editorConfig,
|
||||||
|
editorState: initialValue && !editorRef.current // Only set initial state if editor hasn't been initialized yet with a value
|
||||||
|
? () => {
|
||||||
|
const root = $getRoot();
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
|
paragraph.append($createTextNode(initialValue));
|
||||||
|
root.append(paragraph);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={editorWrapperRef} onBlur={handleWrapperBlur} tabIndex={-1} style={{ border: '1px solid #ccc', position: 'relative', marginTop: '5px' }}>
|
||||||
|
<LexicalComposer initialConfig={localEditorConfig}>
|
||||||
|
<EditorInitializer editorRef={editorRef} />
|
||||||
|
<ToolbarPlugin />
|
||||||
|
<div style={{position: 'relative'}}> {/* Container for ContentEditable and Placeholder */}
|
||||||
|
<RichTextPlugin
|
||||||
|
contentEditable={<ContentEditable style={{ minHeight: '150px', padding: '10px', outline: 'none', resize: 'vertical', userSelect: 'text' }} />}
|
||||||
|
placeholder={<div style={{ position: 'absolute', top: '10px', left: '10px', color: '#aaa', pointerEvents: 'none', userSelect: 'none' }}>Enter text...</div>}
|
||||||
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<HistoryPlugin />
|
||||||
|
<OnChangePlugin onChange={handleChange} />
|
||||||
|
<LinkPlugin /> {/* LinkPlugin provides TOGGLE_LINK_COMMAND */}
|
||||||
|
<ListPlugin />
|
||||||
|
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
|
||||||
|
</LexicalComposer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper component to get editor instance and attach it to a ref
|
||||||
|
function EditorInitializer({ editorRef }: { editorRef: React.MutableRefObject<LexicalEditor | null> }) {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
useEffect(() => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
return () => {
|
||||||
|
editorRef.current = null;
|
||||||
|
};
|
||||||
|
}, [editor, editorRef]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ToolbarPlugin() {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const [isLink, setIsLink] = useState(false);
|
||||||
|
const [isBold, setIsBold] = useState(false);
|
||||||
|
// Add states for other formats if needed, e.g., selectedTextColor, selectedBgColor
|
||||||
|
|
||||||
|
const updateToolbar = useCallback(() => {
|
||||||
|
const selection = $getSelection();
|
||||||
|
if ($isRangeSelection(selection)) {
|
||||||
|
setIsBold(selection.hasFormat('bold'));
|
||||||
|
// Update link state
|
||||||
|
const node = selection.anchor.getNode();
|
||||||
|
const parent = node.getParent();
|
||||||
|
setIsLink(parent?.getType() === 'link' || node.getType() === 'link');
|
||||||
|
// Could also get current text color/bg color here if needed
|
||||||
|
// const style = selection.style;
|
||||||
|
// setSelectedTextColor(style.color);
|
||||||
|
// setSelectedBgColor(style.backgroundColor);
|
||||||
|
}
|
||||||
|
}, [editor]); // editor dependency is implicit via useLexicalComposerContext
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return mergeRegister(
|
||||||
|
editor.registerCommand(
|
||||||
|
SELECTION_CHANGE_COMMAND,
|
||||||
|
() => {
|
||||||
|
updateToolbar();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_LOW,
|
||||||
|
),
|
||||||
|
// Potentially other listeners here, e.g., for text format changes
|
||||||
|
);
|
||||||
|
}, [editor, updateToolbar]);
|
||||||
|
|
||||||
|
|
||||||
|
const toggleBold = () => {
|
||||||
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleLink = useCallback(() => {
|
||||||
|
if (isLink) {
|
||||||
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); // Remove link
|
||||||
|
} else {
|
||||||
|
const url = prompt('Enter link URL:');
|
||||||
|
if (url) {
|
||||||
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editor, isLink]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '8px', borderBottom: '1px solid #ccc', display: 'flex', gap: '8px', userSelect: 'none' }}
|
||||||
|
onMouseDown={(e) => e.preventDefault()} // Prevent editor blur when clicking toolbar
|
||||||
|
>
|
||||||
|
<button onClick={toggleBold} style={{ fontWeight: isBold ? 'bold' : 'normal' }}>B</button>
|
||||||
|
<button onClick={toggleLink} style={{ fontWeight: isLink ? 'bold' : 'normal' }}>{isLink ? 'Unlink' : 'Link'}</button>
|
||||||
|
<button onClick={applyTextColor}>Color</button>
|
||||||
|
<button onClick={applyBackgroundColor}>BG Color</button>
|
||||||
|
{/* Add more buttons here */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStyleToSelection(editor: LexicalEditor, styleKey: string, styleValue: string) {
|
||||||
|
editor.update(() => {
|
||||||
|
const selection = $getSelection();
|
||||||
|
if ($isRangeSelection(selection)) {
|
||||||
|
$patchStyleText(selection, { [styleKey]: styleValue });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTextColor() {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const color = prompt('Enter text color (e.g., red, #FF0000):');
|
||||||
|
if (color) {
|
||||||
|
applyStyleToSelection(editor, 'color', color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBackgroundColor() {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
const color = prompt('Enter background color (e.g., yellow, #FFFF00):');
|
||||||
|
if (color) {
|
||||||
|
// For background color, Lexical often uses 'highlight' format or direct background-color style
|
||||||
|
// Using FORMAT_TEXT_COMMAND for 'highlight' might be one way if a plugin handles it.
|
||||||
|
// Or, apply direct style:
|
||||||
|
applyStyleToSelection(editor, 'background-color', color);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ import { sanitiseCue } from 'ontime-utils';
|
|||||||
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
|
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
|
||||||
import * as Editor from '../../../editors/editor-utils/EditorUtils';
|
import * as Editor from '../../../editors/editor-utils/EditorUtils';
|
||||||
import { type EditorUpdateFields } from '../EventEditor';
|
import { type EditorUpdateFields } from '../EventEditor';
|
||||||
|
import RichTextEditor from '../../../../common/components/input/rich-text-editor/RichTextEditor';
|
||||||
|
|
||||||
import EventTextArea from './EventTextArea';
|
|
||||||
import EventTextInput from './EventTextInput';
|
import EventTextInput from './EventTextInput';
|
||||||
|
|
||||||
import style from '../EventEditor.module.scss';
|
import style from '../EventEditor.module.scss';
|
||||||
@@ -49,7 +49,10 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => {
|
|||||||
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
||||||
</div>
|
</div>
|
||||||
<EventTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
<EventTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
||||||
<EventTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
|
<div>
|
||||||
|
<Editor.Label>Note</Editor.Label>
|
||||||
|
<RichTextEditor initialValue={note} onChange={(newNote) => handleSubmit('note', newNote)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import { type CSSProperties, useCallback, useRef } from 'react';
|
|
||||||
|
|
||||||
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
|
|
||||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
|
||||||
import * as Editor from '../../../editors/editor-utils/EditorUtils';
|
|
||||||
import { EditorUpdateFields } from '../EventEditor';
|
|
||||||
|
|
||||||
interface CountedTextAreaProps {
|
|
||||||
className?: string;
|
|
||||||
field: EditorUpdateFields;
|
|
||||||
label: string;
|
|
||||||
initialValue: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
submitHandler: (field: EditorUpdateFields, value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EventTextArea(props: CountedTextAreaProps) {
|
|
||||||
const { className, field, label, initialValue, style: givenStyles, submitHandler } = props;
|
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
|
||||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
|
||||||
|
|
||||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
|
||||||
submitOnCtrlEnter: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Editor.Label className={className} htmlFor={field} style={givenStyles}>
|
|
||||||
{label}
|
|
||||||
</Editor.Label>
|
|
||||||
<AutoTextArea
|
|
||||||
id={field}
|
|
||||||
inputref={ref}
|
|
||||||
rows={1}
|
|
||||||
size='sm'
|
|
||||||
resize='none'
|
|
||||||
variant='ontime-filled'
|
|
||||||
data-testid='input-textarea'
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
onBlur={onBlur}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { test, expect } from '../../fixtures/override';
|
||||||
|
import { TEST_USER_ID_ADMIN } from '../../../src/common/utils/testing';
|
||||||
|
import { SEED_EVENT_A, SEED_PROJECT_A } from '../../../prisma/seed/standardSeed';
|
||||||
|
|
||||||
|
test.describe('[FEAT] Rich Text Editor in Event Note', () => {
|
||||||
|
test.beforeEach(async ({ page, loginByUserId }) => {
|
||||||
|
// Login as admin user (or any user who can edit events)
|
||||||
|
await loginByUserId(TEST_USER_ID_ADMIN);
|
||||||
|
// Navigate to the cuesheet for a known project
|
||||||
|
await page.goto(`/project/${SEED_PROJECT_A.id}/cuesheet`);
|
||||||
|
// Wait for cuesheet to be ready (e.g., for events to load)
|
||||||
|
await expect(page.locator('div[data-testid^="cue-row-"]')).toHaveCountGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow rich text editing for event notes', async ({ page }) => {
|
||||||
|
// 1. Open an existing event to edit
|
||||||
|
// Assuming SEED_EVENT_A is visible on the cuesheet
|
||||||
|
const eventRowLocator = page.locator(`div[data-testid="cue-row-${SEED_EVENT_A.id}"]`);
|
||||||
|
await eventRowLocator.click();
|
||||||
|
|
||||||
|
// Wait for the event editor modal to appear
|
||||||
|
// Looking for a common element in the editor, e.g., the cue input or title input.
|
||||||
|
// The title input for SEED_EVENT_A has a specific test id pattern from other tests.
|
||||||
|
await expect(page.locator(`input[data-testid="input-textfield-cue-${SEED_EVENT_A.id}"]`)).toBeVisible();
|
||||||
|
|
||||||
|
// 2. Activate Rich Text Editor for 'Note' Field
|
||||||
|
// The note field is initially a placeholder div.
|
||||||
|
// Its value is SEED_EVENT_A.note
|
||||||
|
// The RichTextEditor component's placeholder div has:
|
||||||
|
// - onClick/onFocus to activate
|
||||||
|
// - style: border: '1px solid #ccc', padding: '10px', minHeight: '50px', cursor: 'text'
|
||||||
|
// - role="textbox" (added in tests for RTL, might be present)
|
||||||
|
// - aria-placeholder="Click to edit..."
|
||||||
|
// Let's try to locate it by its initial text content or a combination of properties.
|
||||||
|
// The label "Note" is above it.
|
||||||
|
const noteLabel = page.locator('label:has-text("Note")');
|
||||||
|
const notePlaceholder = noteLabel.locator('+ div[style*="cursor: text"]'); // Sibling div with cursor: text style
|
||||||
|
|
||||||
|
await expect(notePlaceholder).toBeVisible();
|
||||||
|
if (SEED_EVENT_A.note) {
|
||||||
|
await expect(notePlaceholder).toHaveText(SEED_EVENT_A.note);
|
||||||
|
} else {
|
||||||
|
await expect(notePlaceholder).toHaveText('Click to edit...');
|
||||||
|
}
|
||||||
|
|
||||||
|
await notePlaceholder.click(); // Activate the editor
|
||||||
|
|
||||||
|
// Verify the toolbar is now visible
|
||||||
|
// Toolbar buttons: Bold, Link, Color, BG Color
|
||||||
|
const boldButton = page.locator('button:has-text("B")'); // Simple locator for now
|
||||||
|
const linkButton = page.locator('button:has-text("Link")');
|
||||||
|
const colorButton = page.locator('button:has-text("Color")');
|
||||||
|
const bgColorButton = page.locator('button:has-text("BG Color")');
|
||||||
|
|
||||||
|
await expect(boldButton).toBeVisible();
|
||||||
|
await expect(linkButton).toBeVisible();
|
||||||
|
await expect(colorButton).toBeVisible();
|
||||||
|
await expect(bgColorButton).toBeVisible();
|
||||||
|
|
||||||
|
// Locate the ContentEditable area (it's a div with style min-height: '150px')
|
||||||
|
const editorContentArea = page.locator('div[contenteditable="true"]');
|
||||||
|
await expect(editorContentArea).toBeVisible();
|
||||||
|
await expect(editorContentArea).toBeFocused();
|
||||||
|
|
||||||
|
// Clear existing note content (if any) to type fresh
|
||||||
|
// This can be done by selecting all and deleting, or Lexical's CLEAR_EDITOR_COMMAND
|
||||||
|
// For E2E, page.keyboard.press('Control+A') then page.keyboard.press('Delete') is common.
|
||||||
|
await editorContentArea.press('Control+A'); // or 'Meta+A' on macOS
|
||||||
|
await editorContentArea.press('Delete');
|
||||||
|
await expect(editorContentArea).toHaveText('');
|
||||||
|
|
||||||
|
|
||||||
|
// 3. Test Bold Functionality
|
||||||
|
const boldText = 'This text will be bold.';
|
||||||
|
await editorContentArea.type(boldText);
|
||||||
|
await editorContentArea.selectText(); // Select the typed text
|
||||||
|
await boldButton.click();
|
||||||
|
// Verify bold: Lexical usually wraps bold text in a span with font-weight: bold or a <strong> tag.
|
||||||
|
// Let's assume span with style for now.
|
||||||
|
await expect(editorContentArea.locator('span[style*="font-weight: bold;"]')).toHaveText(boldText);
|
||||||
|
// Or if it uses <strong>: await expect(editorContentArea.locator('strong')).toHaveText(boldText);
|
||||||
|
|
||||||
|
// Clear for next test
|
||||||
|
await editorContentArea.press('Control+A');
|
||||||
|
await editorContentArea.press('Delete');
|
||||||
|
|
||||||
|
// 4. Test Link Functionality
|
||||||
|
const linkText = 'This is a link';
|
||||||
|
const testUrl = 'https://example.com';
|
||||||
|
page.on('dialog', dialog => dialog.accept(testUrl)); // Handle prompt
|
||||||
|
await editorContentArea.type(linkText);
|
||||||
|
await editorContentArea.selectText();
|
||||||
|
await linkButton.click();
|
||||||
|
// Verify link: Check for an <a> tag
|
||||||
|
const linkElement = editorContentArea.locator(`a[href="${testUrl}"]`);
|
||||||
|
await expect(linkElement).toBeVisible();
|
||||||
|
await expect(linkElement).toHaveText(linkText);
|
||||||
|
page.off('dialog', () => {}); // Remove listener if it's a one-time thing per action
|
||||||
|
|
||||||
|
// Clear for next test
|
||||||
|
await editorContentArea.press('Control+A');
|
||||||
|
await editorContentArea.press('Delete');
|
||||||
|
|
||||||
|
// 5. Test Text Color Functionality
|
||||||
|
const colorText = 'This text is red';
|
||||||
|
const redColor = 'rgb(255, 0, 0)';
|
||||||
|
page.on('dialog', dialog => dialog.accept(redColor)); // Handle prompt
|
||||||
|
await editorContentArea.type(colorText);
|
||||||
|
await editorContentArea.selectText();
|
||||||
|
await colorButton.click();
|
||||||
|
// Verify text color
|
||||||
|
await expect(editorContentArea.locator(`span[style*="color: ${redColor};"]`)).toHaveText(colorText);
|
||||||
|
page.off('dialog', () => {});
|
||||||
|
|
||||||
|
// Clear for next test
|
||||||
|
await editorContentArea.press('Control+A');
|
||||||
|
await editorContentArea.press('Delete');
|
||||||
|
|
||||||
|
// 6. Test Background Color Functionality
|
||||||
|
const bgColorText = 'This text has yellow background';
|
||||||
|
const yellowBgColor = 'rgb(255, 255, 0)';
|
||||||
|
page.on('dialog', dialog => dialog.accept(yellowBgColor)); // Handle prompt
|
||||||
|
await editorContentArea.type(bgColorText);
|
||||||
|
await editorContentArea.selectText();
|
||||||
|
await bgColorButton.click();
|
||||||
|
// Verify background color
|
||||||
|
await expect(editorContentArea.locator(`span[style*="background-color: ${yellowBgColor};"]`)).toHaveText(bgColorText);
|
||||||
|
page.off('dialog', () => {});
|
||||||
|
|
||||||
|
|
||||||
|
// 7. Save and Verify
|
||||||
|
// Combine some formatting for the save test
|
||||||
|
await editorContentArea.press('Control+A');
|
||||||
|
await editorContentArea.press('Delete');
|
||||||
|
|
||||||
|
const finalNoteText = "Final Note: Bold, Link, and Red.";
|
||||||
|
await editorContentArea.type(finalNoteText);
|
||||||
|
|
||||||
|
// Apply Bold to "Bold"
|
||||||
|
await editorContentArea.press('Home'); // Go to start
|
||||||
|
for(let i=0; i < "Final Note: ".length; i++) await page.keyboard.press('ArrowRight'); // Move cursor
|
||||||
|
await page.keyboard.down('Shift');
|
||||||
|
for(let i=0; i < "Bold".length; i++) await page.keyboard.press('ArrowRight'); // Select "Bold"
|
||||||
|
await page.keyboard.up('Shift');
|
||||||
|
await boldButton.click();
|
||||||
|
|
||||||
|
// Apply Link to "Link" (re-register dialog handler)
|
||||||
|
page.on('dialog', dialog => dialog.accept(testUrl));
|
||||||
|
await editorContentArea.press('End'); // Go to end
|
||||||
|
for(let i=0; i < ", and Red.".length; i++) await page.keyboard.press('ArrowLeft');
|
||||||
|
await page.keyboard.down('Shift');
|
||||||
|
for(let i=0; i < "Link".length; i++) await page.keyboard.press('ArrowLeft');
|
||||||
|
await page.keyboard.up('Shift');
|
||||||
|
await linkButton.click();
|
||||||
|
page.off('dialog', () => {});
|
||||||
|
|
||||||
|
// Apply Red color to "Red" (re-register dialog handler)
|
||||||
|
page.on('dialog', dialog => dialog.accept(redColor));
|
||||||
|
await editorContentArea.press('End');
|
||||||
|
await page.keyboard.press('ArrowLeft'); // for the dot
|
||||||
|
await page.keyboard.down('Shift');
|
||||||
|
for(let i=0; i < "Red".length; i++) await page.keyboard.press('ArrowLeft');
|
||||||
|
await page.keyboard.up('Shift');
|
||||||
|
await colorButton.click();
|
||||||
|
page.off('dialog', () => {});
|
||||||
|
|
||||||
|
// Click save button in the event editor
|
||||||
|
// Assuming a common save button testid or text
|
||||||
|
const saveButton = page.locator('button:has-text("Save")'); // Or a more specific selector
|
||||||
|
await saveButton.click();
|
||||||
|
|
||||||
|
// Wait for modal to close (e.g., the save button is no longer visible)
|
||||||
|
await expect(saveButton).not.toBeVisible();
|
||||||
|
// Or check that the cuesheet is active again
|
||||||
|
await expect(page.locator('div[data-testid^="cue-row-"]')).toHaveCountGreaterThanOrEqual(1);
|
||||||
|
|
||||||
|
// Re-open the same event
|
||||||
|
await eventRowLocator.click();
|
||||||
|
await expect(page.locator(`input[data-testid="input-textfield-cue-${SEED_EVENT_A.id}"]`)).toBeVisible(); // Wait for editor
|
||||||
|
|
||||||
|
// Activate the note editor again
|
||||||
|
const persistedNotePlaceholder = noteLabel.locator('+ div[style*="cursor: text"]');
|
||||||
|
await persistedNotePlaceholder.click();
|
||||||
|
|
||||||
|
const persistedEditorContentArea = page.locator('div[contenteditable="true"]');
|
||||||
|
await expect(persistedEditorContentArea).toBeVisible();
|
||||||
|
|
||||||
|
// Verify persisted content
|
||||||
|
// This is the most complex part, as the exact HTML structure needs to be known.
|
||||||
|
// Example: Check for bold part
|
||||||
|
await expect(persistedEditorContentArea.locator('span[style*="font-weight: bold;"]')).toContainText("Bold");
|
||||||
|
// Example: Check for link part
|
||||||
|
await expect(persistedEditorContentArea.locator(`a[href="${testUrl}"]`)).toContainText("Link");
|
||||||
|
// Example: Check for colored part
|
||||||
|
await expect(persistedEditorContentArea.locator(`span[style*="color: ${redColor};"]`)).toContainText("Red");
|
||||||
|
|
||||||
|
// Overall text check might be too simple if HTML is involved, but good for a basic check
|
||||||
|
// await expect(persistedEditorContentArea).toContainText("Final Note: Bold, Link, and Red.");
|
||||||
|
// A more robust check would be to get the innerHTML and parse it or use snapshot testing.
|
||||||
|
|
||||||
|
// Close the editor (e.g., click cancel or save again)
|
||||||
|
const cancelButton = page.locator('button:has-text("Cancel")'); // Or a more specific selector
|
||||||
|
await cancelButton.click();
|
||||||
|
await expect(cancelButton).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
Generated
+265
-8
@@ -110,6 +110,12 @@ importers:
|
|||||||
'@fontsource/open-sans':
|
'@fontsource/open-sans':
|
||||||
specifier: ^5.0.28
|
specifier: ^5.0.28
|
||||||
version: 5.0.28
|
version: 5.0.28
|
||||||
|
'@lexical/react':
|
||||||
|
specifier: ^0.16.1
|
||||||
|
version: 0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.27)
|
||||||
|
'@lexical/rich-text':
|
||||||
|
specifier: ^0.16.1
|
||||||
|
version: 0.16.1
|
||||||
'@mantine/hooks':
|
'@mantine/hooks':
|
||||||
specifier: ^7.17.2
|
specifier: ^7.17.2
|
||||||
version: 7.17.2(react@18.3.1)
|
version: 7.17.2(react@18.3.1)
|
||||||
@@ -143,6 +149,9 @@ importers:
|
|||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^10.10.0
|
specifier: ^10.10.0
|
||||||
version: 10.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 10.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
lexical:
|
||||||
|
specifier: ^0.16.1
|
||||||
|
version: 0.16.1
|
||||||
prismjs:
|
prismjs:
|
||||||
specifier: ^1.29.0
|
specifier: ^1.29.0
|
||||||
version: 1.29.0
|
version: 1.29.0
|
||||||
@@ -299,7 +308,7 @@ importers:
|
|||||||
version: 2.8.5
|
version: 2.8.5
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.0.1
|
specifier: ^16.0.1
|
||||||
version: 16.3.1
|
version: 16.5.0
|
||||||
express:
|
express:
|
||||||
specifier: 5.1.0
|
specifier: 5.1.0
|
||||||
version: 5.1.0
|
version: 5.1.0
|
||||||
@@ -1766,6 +1775,77 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||||
|
|
||||||
|
'@lexical/clipboard@0.16.1':
|
||||||
|
resolution: {integrity: sha512-0dWs/SwKS5KPpuf6fUVVt9vSCl6HAqcDGhSITw/okv0rrIlXTUT6WhVsMJtXfFxTyVvwMeOecJHvQH3i/jRQtA==}
|
||||||
|
|
||||||
|
'@lexical/code@0.16.1':
|
||||||
|
resolution: {integrity: sha512-pOC28rRZ2XkmI2nIJm50DbKaCJtk5D0o7r6nORYp4i0z+lxt5Sf2m82DL9ksUHJRqKy87pwJDpoWvJ2SAI0ohw==}
|
||||||
|
|
||||||
|
'@lexical/devtools-core@0.16.1':
|
||||||
|
resolution: {integrity: sha512-8CvGERGL7ySDVGLU+YPeq+JupIXsOFlXa3EuJ88koLKqXxYenwMleZgGqayFp6lCP78xqPKnATVeoOZUt/NabQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17.x'
|
||||||
|
react-dom: '>=17.x'
|
||||||
|
|
||||||
|
'@lexical/dragon@0.16.1':
|
||||||
|
resolution: {integrity: sha512-Rvd60GIYN5kpjjBumS34EnNbBaNsoseI0AlzOdtIV302jiHPCLH0noe9kxzu9nZy+MZmjZy8Dx2zTbQT2mueRw==}
|
||||||
|
|
||||||
|
'@lexical/hashtag@0.16.1':
|
||||||
|
resolution: {integrity: sha512-G+YOxStAKs3q1utqm9KR4D4lCkwIH52Rctm4RgaVTI+4lvTaybeDRGFV75P/pI/qlF7/FvAYHTYEzCjtC3GNMQ==}
|
||||||
|
|
||||||
|
'@lexical/history@0.16.1':
|
||||||
|
resolution: {integrity: sha512-WQhScx0TJeKSQAnEkRpIaWdUXqirrNrom2MxbBUc/32zEUMm9FzV7nRGknvUabEFUo7vZq6xTZpOExQJqHInQA==}
|
||||||
|
|
||||||
|
'@lexical/html@0.16.1':
|
||||||
|
resolution: {integrity: sha512-vbtAdCvQ3PaAqa5mFmtmrvbiAvjCu1iXBAJ0bsHqFXCF2Sba5LwHVe8dUAOTpfEZEMbiHfjul6b5fj4vNPGF2A==}
|
||||||
|
|
||||||
|
'@lexical/link@0.16.1':
|
||||||
|
resolution: {integrity: sha512-zG36gEnEqbIe6tK/MhXi7wn/XMY/zdivnPcOY5WyC3derkEezeLSSIFsC1u5UNeK5pbpNMSy4LDpLhi1Ww4Y5w==}
|
||||||
|
|
||||||
|
'@lexical/list@0.16.1':
|
||||||
|
resolution: {integrity: sha512-i9YhLAh5N6YO9dP+R1SIL9WEdCKeTiQQYVUzj84vDvX5DIBxMPUjTmMn3LXu9T+QO3h1s2L/vJusZASrl45eAw==}
|
||||||
|
|
||||||
|
'@lexical/mark@0.16.1':
|
||||||
|
resolution: {integrity: sha512-CZRGMLcxn5D+jzf1XnH+Z+uUugmpg1mBwTbGybCPm8UWpBrKDHkrscfMgWz62iRWz0cdVjM5+0zWpNElxFTRjQ==}
|
||||||
|
|
||||||
|
'@lexical/markdown@0.16.1':
|
||||||
|
resolution: {integrity: sha512-0sBLttMvfQO/hVaIqpHdvDowpgV2CoRuWo2CNwvRLZPPWvPVjL4Nkb73wmi8zAZsAOTbX2aw+g4m/+k5oJqNig==}
|
||||||
|
|
||||||
|
'@lexical/offset@0.16.1':
|
||||||
|
resolution: {integrity: sha512-/i2J04lQmFeydUZIF8tKXLQTXiJDTQ6GRnkfv1OpxU4amc0rwGa7+qAz/PuF1n58rP6InpLmSHxgY5JztXa2jw==}
|
||||||
|
|
||||||
|
'@lexical/overflow@0.16.1':
|
||||||
|
resolution: {integrity: sha512-xh5YpoxwA7K4wgMQF/Sjl8sdjaxqesLCtH5ZrcMsaPlmucDIEEs+i8xxk+kDUTEY7y+3FvRxs4lGNgX8RVWkvQ==}
|
||||||
|
|
||||||
|
'@lexical/plain-text@0.16.1':
|
||||||
|
resolution: {integrity: sha512-GjY4ylrBZIaAVIF8IFnmW0XGyHAuRmWA6gKB8iTTlsjgFrCHFIYC74EeJSp309O0Hflg9rRBnKoX1TYruFHVwA==}
|
||||||
|
|
||||||
|
'@lexical/react@0.16.1':
|
||||||
|
resolution: {integrity: sha512-SsGgLt9iKfrrMRy9lFb6ROVPUYOgv6b+mCn9Al+TLqs/gBReDBi3msA7m526nrtBUKYUnjHdQ1QXIJzuKgOxcg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17.x'
|
||||||
|
react-dom: '>=17.x'
|
||||||
|
|
||||||
|
'@lexical/rich-text@0.16.1':
|
||||||
|
resolution: {integrity: sha512-4uEVXJur7tdSbqbmsToCW4YVm0AMh4y9LK077Yq2O9hSuA5dqpI8UbTDnxZN2D7RfahNvwlqp8eZKFB1yeiJGQ==}
|
||||||
|
|
||||||
|
'@lexical/selection@0.16.1':
|
||||||
|
resolution: {integrity: sha512-+nK3RvXtyQvQDq7AZ46JpphmM33pwuulwiRfeXR5T9iFQTtgWOEjsAi/KKX7vGm70BxACfiSxy5QCOgBWFwVJg==}
|
||||||
|
|
||||||
|
'@lexical/table@0.16.1':
|
||||||
|
resolution: {integrity: sha512-GWb0/MM1sVXpi1p2HWWOBldZXASMQ4c6WRNYnRmq7J/aB5N66HqQgJGKp3m66Kz4k1JjhmZfPs7F018qIBhnFQ==}
|
||||||
|
|
||||||
|
'@lexical/text@0.16.1':
|
||||||
|
resolution: {integrity: sha512-Os/nKQegORTrKKN6vL3/FMVszyzyqaotlisPynvTaHTUC+yY4uyjM2hlF93i5a2ixxyiPLF9bDroxUP96TMPXg==}
|
||||||
|
|
||||||
|
'@lexical/utils@0.16.1':
|
||||||
|
resolution: {integrity: sha512-BVyJxDQi/rIxFTDjf2zE7rMDKSuEaeJ4dybHRa/hRERt85gavGByQawSLeQlTjLaYLVsy+x7wCcqh2fNhlLf0g==}
|
||||||
|
|
||||||
|
'@lexical/yjs@0.16.1':
|
||||||
|
resolution: {integrity: sha512-QHw1bmzB/IypIV1tRWMH4hhwE1xX7wV+HxbzBS8oJAkoU5AYXM/kyp/sQicgqiwVfpai1Px7zatOoUDFgbyzHQ==}
|
||||||
|
peerDependencies:
|
||||||
|
yjs: '>=13.5.22'
|
||||||
|
|
||||||
'@malept/cross-spawn-promise@1.1.1':
|
'@malept/cross-spawn-promise@1.1.1':
|
||||||
resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==}
|
resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -3108,10 +3188,6 @@ packages:
|
|||||||
dotenv-expand@5.1.0:
|
dotenv-expand@5.1.0:
|
||||||
resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==}
|
resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==}
|
||||||
|
|
||||||
dotenv@16.3.1:
|
|
||||||
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
|
|
||||||
dotenv@16.5.0:
|
dotenv@16.5.0:
|
||||||
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -3981,6 +4057,9 @@ packages:
|
|||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
isomorphic.js@0.2.5:
|
||||||
|
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
|
||||||
|
|
||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
@@ -4074,6 +4153,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
lexical@0.16.1:
|
||||||
|
resolution: {integrity: sha512-+R05d3+N945OY8pTUjTqQrWoApjC+ctzvjnmNETtx9WmVAaiW0tQVG+AYLt5pDGY8dQXtd4RPorvnxBTECt9SA==}
|
||||||
|
|
||||||
|
lib0@0.2.108:
|
||||||
|
resolution: {integrity: sha512-+3eK/B0SqYoZiQu9fNk4VEc6EX8cb0Li96tPGKgugzoGj/OdRdREtuTLvUW+mtinoB2mFiJjSqOJBIaMkAGhxQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
lines-and-columns@1.2.4:
|
lines-and-columns@1.2.4:
|
||||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
|
|
||||||
@@ -4657,6 +4744,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18.3.1
|
react: ^18.3.1
|
||||||
|
|
||||||
|
react-error-boundary@3.1.4:
|
||||||
|
resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==}
|
||||||
|
engines: {node: '>=10', npm: '>=6'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.13.1'
|
||||||
|
|
||||||
react-fast-compare@3.2.1:
|
react-fast-compare@3.2.1:
|
||||||
resolution: {integrity: sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==}
|
resolution: {integrity: sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==}
|
||||||
|
|
||||||
@@ -5646,6 +5739,10 @@ packages:
|
|||||||
yauzl@2.10.0:
|
yauzl@2.10.0:
|
||||||
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
||||||
|
|
||||||
|
yjs@13.6.27:
|
||||||
|
resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==}
|
||||||
|
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
|
||||||
|
|
||||||
yocto-queue@0.1.0:
|
yocto-queue@0.1.0:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -7169,6 +7266,151 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.1
|
'@jridgewell/resolve-uri': 3.1.1
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
'@lexical/clipboard@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/html': 0.16.1
|
||||||
|
'@lexical/list': 0.16.1
|
||||||
|
'@lexical/selection': 0.16.1
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/code@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
prismjs: 1.29.0
|
||||||
|
|
||||||
|
'@lexical/devtools-core@0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/html': 0.16.1
|
||||||
|
'@lexical/link': 0.16.1
|
||||||
|
'@lexical/mark': 0.16.1
|
||||||
|
'@lexical/table': 0.16.1
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
|
'@lexical/dragon@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/hashtag@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/history@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/html@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/selection': 0.16.1
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/link@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/list@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/mark@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/markdown@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/code': 0.16.1
|
||||||
|
'@lexical/link': 0.16.1
|
||||||
|
'@lexical/list': 0.16.1
|
||||||
|
'@lexical/rich-text': 0.16.1
|
||||||
|
'@lexical/text': 0.16.1
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/offset@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/overflow@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/plain-text@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/clipboard': 0.16.1
|
||||||
|
'@lexical/selection': 0.16.1
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/react@0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yjs@13.6.27)':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/clipboard': 0.16.1
|
||||||
|
'@lexical/code': 0.16.1
|
||||||
|
'@lexical/devtools-core': 0.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@lexical/dragon': 0.16.1
|
||||||
|
'@lexical/hashtag': 0.16.1
|
||||||
|
'@lexical/history': 0.16.1
|
||||||
|
'@lexical/link': 0.16.1
|
||||||
|
'@lexical/list': 0.16.1
|
||||||
|
'@lexical/mark': 0.16.1
|
||||||
|
'@lexical/markdown': 0.16.1
|
||||||
|
'@lexical/overflow': 0.16.1
|
||||||
|
'@lexical/plain-text': 0.16.1
|
||||||
|
'@lexical/rich-text': 0.16.1
|
||||||
|
'@lexical/selection': 0.16.1
|
||||||
|
'@lexical/table': 0.16.1
|
||||||
|
'@lexical/text': 0.16.1
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
'@lexical/yjs': 0.16.1(yjs@13.6.27)
|
||||||
|
lexical: 0.16.1
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
react-error-boundary: 3.1.4(react@18.3.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- yjs
|
||||||
|
|
||||||
|
'@lexical/rich-text@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/clipboard': 0.16.1
|
||||||
|
'@lexical/selection': 0.16.1
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/selection@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/table@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/utils': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/text@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/utils@0.16.1':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/list': 0.16.1
|
||||||
|
'@lexical/selection': 0.16.1
|
||||||
|
'@lexical/table': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
|
||||||
|
'@lexical/yjs@0.16.1(yjs@13.6.27)':
|
||||||
|
dependencies:
|
||||||
|
'@lexical/offset': 0.16.1
|
||||||
|
lexical: 0.16.1
|
||||||
|
yjs: 13.6.27
|
||||||
|
|
||||||
'@malept/cross-spawn-promise@1.1.1':
|
'@malept/cross-spawn-promise@1.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@@ -7338,7 +7580,7 @@ snapshots:
|
|||||||
'@babel/core': 7.23.6
|
'@babel/core': 7.23.6
|
||||||
'@sentry/babel-plugin-component-annotate': 2.16.1
|
'@sentry/babel-plugin-component-annotate': 2.16.1
|
||||||
'@sentry/cli': 2.23.0(encoding@0.1.13)
|
'@sentry/cli': 2.23.0(encoding@0.1.13)
|
||||||
dotenv: 16.3.1
|
dotenv: 16.5.0
|
||||||
find-up: 5.0.0
|
find-up: 5.0.0
|
||||||
glob: 9.3.2
|
glob: 9.3.2
|
||||||
magic-string: 0.30.8
|
magic-string: 0.30.8
|
||||||
@@ -8721,8 +8963,6 @@ snapshots:
|
|||||||
|
|
||||||
dotenv-expand@5.1.0: {}
|
dotenv-expand@5.1.0: {}
|
||||||
|
|
||||||
dotenv@16.3.1: {}
|
|
||||||
|
|
||||||
dotenv@16.5.0: {}
|
dotenv@16.5.0: {}
|
||||||
|
|
||||||
dotenv@9.0.2: {}
|
dotenv@9.0.2: {}
|
||||||
@@ -9844,6 +10084,8 @@ snapshots:
|
|||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
|
isomorphic.js@0.2.5: {}
|
||||||
|
|
||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/cliui': 8.0.2
|
'@isaacs/cliui': 8.0.2
|
||||||
@@ -9969,6 +10211,12 @@ snapshots:
|
|||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
type-check: 0.4.0
|
type-check: 0.4.0
|
||||||
|
|
||||||
|
lexical@0.16.1: {}
|
||||||
|
|
||||||
|
lib0@0.2.108:
|
||||||
|
dependencies:
|
||||||
|
isomorphic.js: 0.2.5
|
||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
@@ -10489,6 +10737,11 @@ snapshots:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
scheduler: 0.23.2
|
scheduler: 0.23.2
|
||||||
|
|
||||||
|
react-error-boundary@3.1.4(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.24.5
|
||||||
|
react: 18.3.1
|
||||||
|
|
||||||
react-fast-compare@3.2.1: {}
|
react-fast-compare@3.2.1: {}
|
||||||
|
|
||||||
react-fast-compare@3.2.2: {}
|
react-fast-compare@3.2.2: {}
|
||||||
@@ -11539,6 +11792,10 @@ snapshots:
|
|||||||
buffer-crc32: 0.2.13
|
buffer-crc32: 0.2.13
|
||||||
fd-slicer: 1.1.0
|
fd-slicer: 1.1.0
|
||||||
|
|
||||||
|
yjs@13.6.27:
|
||||||
|
dependencies:
|
||||||
|
lib0: 0.2.108
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zip-stream@4.1.1:
|
zip-stream@4.1.1:
|
||||||
|
|||||||
Reference in New Issue
Block a user