mirror of
https://github.com/cpvalente/ontime.git
synced 2026-07-26 10:38:55 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f80e957036 |
@@ -12,6 +12,8 @@
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@fontsource/open-sans": "^5.0.28",
|
||||
"@lexical/react": "^0.16.1",
|
||||
"@lexical/rich-text": "^0.16.1",
|
||||
"@mantine/hooks": "^7.17.2",
|
||||
"@sentry/react": "^8.43.0",
|
||||
"@table-nav/react": "^0.0.7",
|
||||
@@ -34,7 +36,8 @@
|
||||
"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.16.1"
|
||||
},
|
||||
"scripts": {
|
||||
"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 * as Editor from '../../../editors/editor-utils/EditorUtils';
|
||||
import { type EditorUpdateFields } from '../EventEditor';
|
||||
import RichTextEditor from '../../../../common/components/input/rich-text-editor/RichTextEditor';
|
||||
|
||||
import EventTextArea from './EventTextArea';
|
||||
import EventTextInput from './EventTextInput';
|
||||
|
||||
import style from '../EventEditor.module.scss';
|
||||
@@ -49,7 +49,10 @@ const EventEditorTitles = (props: EventEditorTitlesProps) => {
|
||||
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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':
|
||||
specifier: ^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':
|
||||
specifier: ^7.17.2
|
||||
version: 7.17.2(react@18.3.1)
|
||||
@@ -143,6 +149,9 @@ importers:
|
||||
framer-motion:
|
||||
specifier: ^10.10.0
|
||||
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:
|
||||
specifier: ^1.29.0
|
||||
version: 1.29.0
|
||||
@@ -299,7 +308,7 @@ importers:
|
||||
version: 2.8.5
|
||||
dotenv:
|
||||
specifier: ^16.0.1
|
||||
version: 16.3.1
|
||||
version: 16.5.0
|
||||
express:
|
||||
specifier: 5.1.0
|
||||
version: 5.1.0
|
||||
@@ -1766,6 +1775,77 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
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':
|
||||
resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -3108,10 +3188,6 @@ packages:
|
||||
dotenv-expand@5.1.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3981,6 +4057,9 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
isomorphic.js@0.2.5:
|
||||
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
@@ -4074,6 +4153,14 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
@@ -4657,6 +4744,12 @@ packages:
|
||||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==}
|
||||
|
||||
@@ -5646,6 +5739,10 @@ packages:
|
||||
yauzl@2.10.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7169,6 +7266,151 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.1
|
||||
'@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':
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
@@ -7338,7 +7580,7 @@ snapshots:
|
||||
'@babel/core': 7.23.6
|
||||
'@sentry/babel-plugin-component-annotate': 2.16.1
|
||||
'@sentry/cli': 2.23.0(encoding@0.1.13)
|
||||
dotenv: 16.3.1
|
||||
dotenv: 16.5.0
|
||||
find-up: 5.0.0
|
||||
glob: 9.3.2
|
||||
magic-string: 0.30.8
|
||||
@@ -8721,8 +8963,6 @@ snapshots:
|
||||
|
||||
dotenv-expand@5.1.0: {}
|
||||
|
||||
dotenv@16.3.1: {}
|
||||
|
||||
dotenv@16.5.0: {}
|
||||
|
||||
dotenv@9.0.2: {}
|
||||
@@ -9844,6 +10084,8 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
isomorphic.js@0.2.5: {}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
@@ -9969,6 +10211,12 @@ snapshots:
|
||||
prelude-ls: 1.2.1
|
||||
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: {}
|
||||
|
||||
locate-path@6.0.0:
|
||||
@@ -10489,6 +10737,11 @@ snapshots:
|
||||
react: 18.3.1
|
||||
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.2: {}
|
||||
@@ -11539,6 +11792,10 @@ snapshots:
|
||||
buffer-crc32: 0.2.13
|
||||
fd-slicer: 1.1.0
|
||||
|
||||
yjs@13.6.27:
|
||||
dependencies:
|
||||
lib0: 0.2.108
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zip-stream@4.1.1:
|
||||
|
||||
Reference in New Issue
Block a user