@@ -19,13 +19,19 @@ import {
1919 type InlinePanelController ,
2020} from './inlinePanel' ;
2121import { createDiffExtension , type DiffProps } from './diffView' ;
22+ import {
23+ createEditorActionsController ,
24+ type EditorActionsCallbacks ,
25+ type EditorActionsController ,
26+ } from './editorActions' ;
2227import {
2328 formatQuery ,
2429 CypherLanguageService ,
2530 type DbSchema ,
2631} from '@neo4j-cypher/language-support' ;
2732import debounce from 'lodash.debounce' ;
28- import { Component , createRef } from 'react' ;
33+ import { Component , createRef , type ReactNode } from 'react' ;
34+ import { createPortal } from 'react-dom' ;
2935import { DEBOUNCE_TIME } from './constants' ;
3036import {
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
201213export 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
313327const ExternalEdit = Annotation . define < boolean > ( ) ;
314328const 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