Skip to content

Commit 9c65003

Browse files
authored
add editor action buttons overlay to CypherEditor (#697)
* add editor action buttons overlay to CypherEditor * adding react-dom as a peer dependency
1 parent 77c433c commit 9c65003

7 files changed

Lines changed: 362 additions & 15 deletions

File tree

.changeset/witty-otters-wave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@neo4j-cypher/react-codemirror': patch
3+
---
4+
5+
Declares `react-dom` as a peer dependency, since `CypherEditor` now imports `createPortal` from it to render the editor action buttons overlay

packages/react-codemirror-playground/src/App.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,35 @@ function InlinePanelDemo({ onClose }: { onClose: () => void }) {
3333
);
3434
}
3535

36+
function EditorActionsDemo() {
37+
return (
38+
<div style={{ display: 'flex', gap: 4, padding: 4 }}>
39+
<button
40+
style={{
41+
background: '#e5e7eb',
42+
borderRadius: 4,
43+
padding: '2px 8px',
44+
fontSize: 12,
45+
}}
46+
>
47+
48+
</button>
49+
<button
50+
onClick={() => {}}
51+
style={{
52+
background: '#2563eb',
53+
color: 'white',
54+
borderRadius: 4,
55+
padding: '2px 10px',
56+
fontSize: 12,
57+
}}
58+
>
59+
Run
60+
</button>
61+
</div>
62+
);
63+
}
64+
3665
const demos = {
3766
allTokenTypes: `MATCH (variable :Label)-[:REL_TYPE]->()
3867
WHERE variable.property = "String"
@@ -202,7 +231,9 @@ export function App() {
202231
<button
203232
key={demoName}
204233
className={`hover:bg-blue-600 text-white font-bold py-1 px-3 rounded
205-
${selectedDemoName === demoName ? 'bg-blue-600' : 'bg-blue-400'}`}
234+
${
235+
selectedDemoName === demoName ? 'bg-blue-600' : 'bg-blue-400'
236+
}`}
206237
onClick={() => {
207238
setSelectedDemoName(demoName);
208239
setValue(demos[demoName]);
@@ -215,7 +246,8 @@ export function App() {
215246
))}
216247
</div>
217248
<CypherEditor
218-
className="border-2 border-gray-100 dark:border-gray-400 text-sm"
249+
className="border-2 border-gray-100 dark:border-gray-400 text-sm max-h-[200px]"
250+
lineWrap
219251
value={value}
220252
ref={editorRef}
221253
onChange={setValue}
@@ -233,6 +265,7 @@ export function App() {
233265
}}
234266
ariaLabel="Cypher Editor"
235267
inlinePanel={inlinePanel}
268+
editorActions={<EditorActionsDemo />}
236269
diff={diff}
237270
/>
238271
{inlinePanelContainer &&

packages/react-codemirror/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@
7878
"vite": "^4.5.10"
7979
},
8080
"peerDependencies": {
81-
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
81+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
82+
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
8283
},
8384
"engines": {
8485
"node": ">=24.11.1"

packages/react-codemirror/src/CypherEditor.tsx

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@ import {
1919
type InlinePanelController,
2020
} from './inlinePanel';
2121
import { createDiffExtension, type DiffProps } from './diffView';
22+
import {
23+
createEditorActionsController,
24+
type EditorActionsCallbacks,
25+
type EditorActionsController,
26+
} from './editorActions';
2227
import {
2328
formatQuery,
2429
CypherLanguageService,
2530
type DbSchema,
2631
} from '@neo4j-cypher/language-support';
2732
import debounce from 'lodash.debounce';
28-
import { Component, createRef } from 'react';
33+
import { Component, createRef, type ReactNode } from 'react';
34+
import { createPortal } from 'react-dom';
2935
import { DEBOUNCE_TIME } from './constants';
3036
import {
3137
replaceHistory,
@@ -196,6 +202,12 @@ export interface CypherEditorProps {
196202
* Deleted lines are shown as uneditable widgets.
197203
*/
198204
diff?: DiffProps | null;
205+
/**
206+
* React content rendered as a cluster of action buttons (e.g. a context menu
207+
* + Run) pinned to the top-right corner of the editor. Omit (or `null`) to
208+
* hide it.
209+
*/
210+
editorActions?: ReactNode;
199211
}
200212

201213
export type InlinePanelProps = {
@@ -308,7 +320,9 @@ const formatLineNumber =
308320
return a.toString();
309321
};
310322

311-
type CypherEditorState = { cypherSupportEnabled: boolean };
323+
type CypherEditorState = {
324+
editorActionsContainer: HTMLElement | null;
325+
};
312326

313327
const ExternalEdit = Annotation.define<boolean>();
314328
const WorkerURL = new URL('./lang-cypher/lintWorker.mjs', import.meta.url)
@@ -393,6 +407,17 @@ export class CypherEditor extends Component<
393407
editorView: React.MutableRefObject<EditorView> = createRef();
394408
private schemaRef: React.MutableRefObject<CypherConfig> = createRef();
395409
private inlinePanelController: InlinePanelController | null = null;
410+
private editorActionsController: EditorActionsController | null = null;
411+
412+
state: CypherEditorState = {
413+
editorActionsContainer: null,
414+
};
415+
416+
private editorActionsCallbacks: EditorActionsCallbacks = {
417+
onMount: (container) =>
418+
this.setState({ editorActionsContainer: container }),
419+
onUnmount: () => this.setState({ editorActionsContainer: null }),
420+
};
396421

397422
/**
398423
* Format Cypher query
@@ -505,6 +530,7 @@ export class CypherEditor extends Component<
505530
);
506531

507532
this.inlinePanelController = createInlinePanelController();
533+
this.editorActionsController = createEditorActionsController();
508534

509535
const changeListener = this.debouncedOnChange
510536
? [
@@ -575,6 +601,7 @@ export class CypherEditor extends Component<
575601
diffCompartment.of(
576602
this.props.diff ? createDiffExtension(this.props.diff) : [],
577603
),
604+
this.editorActionsController.extension,
578605
],
579606
doc: this.props.value,
580607
});
@@ -596,6 +623,22 @@ export class CypherEditor extends Component<
596623
if (this.props.inlinePanel) {
597624
this.openInlinePanel(this.props.inlinePanel);
598625
}
626+
627+
if (this.props.editorActions != null) {
628+
this.setEditorActions(true);
629+
}
630+
}
631+
632+
private setEditorActions(active: boolean): void {
633+
const view = this.editorView.current;
634+
const controller = this.editorActionsController;
635+
if (!view || !controller) {
636+
return;
637+
}
638+
639+
view.dispatch({
640+
effects: controller.set(active ? this.editorActionsCallbacks : null),
641+
});
599642
}
600643

601644
private openInlinePanel(
@@ -752,6 +795,12 @@ export class CypherEditor extends Component<
752795
});
753796
}
754797

798+
const hadActions = prevProps.editorActions != null;
799+
const hasActions = this.props.editorActions != null;
800+
if (hadActions !== hasActions) {
801+
this.setEditorActions(hasActions);
802+
}
803+
755804
if (prevProps.domEventHandlers !== this.props.domEventHandlers) {
756805
this.editorView.current.dispatch({
757806
effects: domEventHandlerCompartment.reconfigure(
@@ -796,11 +845,16 @@ export class CypherEditor extends Component<
796845
const themeClass =
797846
typeof theme === 'string' ? `cm-theme-${theme}` : 'cm-theme';
798847

848+
const { editorActionsContainer } = this.state;
849+
799850
return (
800851
<div
801852
ref={this.editorContainer}
802853
className={`${themeClass}${className ? ` ${className}` : ''}`}
803-
/>
854+
>
855+
{editorActionsContainer &&
856+
createPortal(this.props.editorActions, editorActionsContainer)}
857+
</div>
804858
);
805859
}
806860
}

0 commit comments

Comments
 (0)