11import { DbSchema , testData } from '@neo4j-cypher/language-support' ;
22import {
33 CypherEditor ,
4+ type DiffProps ,
45 type InlinePanelProps ,
56} from '@neo4j-cypher/react-codemirror' ;
67import { useCallback , useMemo , useRef , useState } from 'react' ;
@@ -33,15 +34,15 @@ function InlinePanelDemo({ onClose }: { onClose: () => void }) {
3334}
3435
3536const demos = {
36- allTokenTypes : `MATCH (variable :Label)-[:REL_TYPE]->()
37- WHERE variable.property = "String"
37+ allTokenTypes : `MATCH (variable :Label)-[:REL_TYPE]->()
38+ WHERE variable.property = "String"
3839 OR namespaced.function() = false
3940 // comment
40- OR $parameter > 2
41+ OR $parameter > 2
4142RETURN variable;` ,
4243 basic : `MATCH (n:Person)
43- WHERE n.name = "Steve"
44- RETURN n
44+ WHERE n.name = "Steve"
45+ RETURN n
4546LIMIT 12;` ,
4647 subqueries : `UNWIND range(1,100) as _
4748CALL {
@@ -50,14 +51,43 @@ CALL {
5051 MATCH path = (source)-[*1..10]->(target)
5152 WITH path, reduce(weight = 0, r IN relationships(path) | weight + r.weight) as Weight
5253 ORDER BY Weight LIMIT 3
53- RETURN length(path) as l, Weight
54- }
54+ RETURN length(path) as l, Weight
55+ }
5556RETURN count(*)` ,
5657 createDatabase : `CREATE DATABASE testdb OPTIONS {existingData: 'use', seedURI:'s3://bucketpath', seedConfig: 'region=eu-west-1', seedCredentials: 'foo;bar'};` ,
5758} as const ;
5859
5960type DemoName = keyof typeof demos ;
6061
62+ const diffExamples : Record < DemoName , { before : string ; after : string } > = {
63+ allTokenTypes : {
64+ before : demos . allTokenTypes ,
65+ after : demos . allTokenTypes . replace (
66+ 'WHERE variable.property = "String"' ,
67+ 'WHERE variable.property = "Changed String"' ,
68+ ) ,
69+ } ,
70+ basic : {
71+ before : demos . basic ,
72+ after : `MATCH (n:Person)
73+ WHERE n.name = "Steve"
74+ WITH n
75+ ORDER BY n.age DESC
76+ RETURN n
77+ LIMIT 12;` ,
78+ } ,
79+ subqueries : {
80+ before : demos . subqueries ,
81+ after : demos . subqueries
82+ . replace ( '[*1..10]' , '[*1..5]' )
83+ . replace ( 'RETURN count(*)' , 'RETURN count(*) LIMIT 1000' ) ,
84+ } ,
85+ createDatabase : {
86+ before : demos . createDatabase ,
87+ after : `CREATE DATABASE testdb OPTIONS {existingData: 'ignore', seedURI:'s3://bucketpath', seedConfig: 'region=eu-west-1', seedCredentials: 'foo;bar', storeFormat: 'aligned'};` ,
88+ } ,
89+ } ;
90+
6191export function App ( ) {
6292 const [ selectedDemoName , setSelectedDemoName ] = useState < DemoName > ( 'basic' ) ;
6393 const [ value , setValue ] = useState < string > ( demos [ selectedDemoName ] ) ;
@@ -74,35 +104,62 @@ export function App() {
74104 ) ;
75105 const [ schemaError , setSchemaError ] = useState < string | null > ( null ) ;
76106
77- const [ isInlinePanelOpen , setIsInlinePanelOpen ] = useState ( false ) ;
107+ const editorRef = useRef < CypherEditor > ( null ) ;
108+
109+ const [ inlinePanel , setInlinePanel ] = useState < InlinePanelProps | null > ( null ) ;
78110 const [ inlinePanelContainer , setInlinePanelContainer ] =
79111 useState < HTMLElement | null > ( null ) ;
80112
81- const closeInlinePanel = useCallback ( ( ) => setIsInlinePanelOpen ( false ) , [ ] ) ;
113+ const openInlinePanel = useCallback ( ( ) => {
114+ const view = editorRef . current ?. editorView . current ;
115+ if ( ! view || inlinePanel !== null ) return ;
116+ const stableContainer = document . createElement ( 'div' ) ;
117+ setInlinePanel ( {
118+ pos : view . state . selection . main . from ,
119+ onMount : ( widgetDiv ) => {
120+ widgetDiv . appendChild ( stableContainer ) ;
121+ setInlinePanelContainer ( stableContainer ) ;
122+ } ,
123+ onUnmount : ( ) => { } ,
124+ } ) ;
125+ } , [ inlinePanel ] ) ;
126+
127+ const closeInlinePanel = useCallback ( ( ) => {
128+ setInlinePanelContainer ( null ) ;
129+ setInlinePanel ( null ) ;
130+ } , [ ] ) ;
131+
132+ const [ diff , setDiff ] = useState < DiffProps | null > ( null ) ;
133+ let beforeValue : string = '' ;
134+ const showDiff = useCallback ( ( ) => {
135+ beforeValue =
136+ editorRef . current ?. editorView . current ?. state . doc . toString ( ) ?? '' ;
137+ setDiff ( { original : beforeValue } ) ;
138+ } , [ ] ) ;
139+ const acceptDiff = useCallback ( ( ) => setDiff ( null ) , [ ] ) ;
140+ const rejectDiff = useCallback ( ( ) => {
141+ setValue ( beforeValue ) ;
142+ setDiff ( null ) ;
143+ } , [ ] ) ;
144+
145+ const loadDemoContent = useCallback ( ( ) => {
146+ const example = diffExamples [ selectedDemoName ] ;
147+ setValue ( example . after ) ;
148+ setDiff ( { original : example . before } ) ;
149+ } , [ selectedDemoName ] ) ;
82150
83151 const extraKeybindings = [
84152 {
85153 key : 'Ctrl-Shift-f' ,
86154 mac : 'Alt-Shift-f' ,
87155 preventDefault : true ,
88156 run : ( ) => {
89- editorRef . current . format ( ) ;
157+ editorRef . current ? .format ( ) ;
90158 return true ;
91159 } ,
92160 } ,
93161 ] ;
94162
95- const editorRef = useRef < CypherEditor > ( null ) ;
96-
97- const inlinePanel : InlinePanelProps | null = isInlinePanelOpen
98- ? {
99- pos :
100- editorRef . current ?. editorView . current ?. state . selection . main . from ?? 0 ,
101- onMount : setInlinePanelContainer ,
102- onUnmount : ( ) => setInlinePanelContainer ( null ) ,
103- }
104- : null ;
105-
106163 const treeData = useMemo ( ( ) => {
107164 return getDebugTree ( value ) ;
108165 } , [ value ] ) ;
@@ -141,14 +198,16 @@ export function App() {
141198 </ button >
142199 </ div >
143200 < div className = "flex gap-1" >
144- { Object . keys ( demos ) . map ( ( demoName : DemoName ) => (
201+ { ( Object . keys ( demos ) as DemoName [ ] ) . map ( ( demoName ) => (
145202 < button
146203 key = { demoName }
147- className = { `hover:bg-blue-600 text-white font-bold py-1 px-3 rounded
204+ className = { `hover:bg-blue-600 text-white font-bold py-1 px-3 rounded
148205 ${ selectedDemoName === demoName ? 'bg-blue-600' : 'bg-blue-400' } ` }
149206 onClick = { ( ) => {
150207 setSelectedDemoName ( demoName ) ;
151208 setValue ( demos [ demoName ] ) ;
209+ setDiff ( null ) ;
210+ closeInlinePanel ( ) ;
152211 } }
153212 >
154213 { demoName }
@@ -174,6 +233,7 @@ export function App() {
174233 } }
175234 ariaLabel = "Cypher Editor"
176235 inlinePanel = { inlinePanel }
236+ diff = { diff }
177237 />
178238 { inlinePanelContainer &&
179239 createPortal (
@@ -182,7 +242,7 @@ export function App() {
182242 ) }
183243 < div className = "flex gap-4" >
184244 < p
185- onClick = { ( ) => editorRef . current . format ( ) }
245+ onClick = { ( ) => editorRef . current ? .format ( ) }
186246 className = "text-blue-500 cursor-pointer hover:text-blue-700"
187247 title = {
188248 window . navigator . userAgent . includes ( 'Mac' )
@@ -193,11 +253,44 @@ export function App() {
193253 Format Query
194254 </ p >
195255 < p
196- onClick = { ( ) => setIsInlinePanelOpen ( ( prevState ) => ! prevState ) }
256+ onClick = { ( ) =>
257+ inlinePanel ? closeInlinePanel ( ) : openInlinePanel ( )
258+ }
197259 className = "text-blue-500 cursor-pointer hover:text-blue-700"
198260 >
199- { isInlinePanelOpen ? 'Close inline panel' : 'Open inline panel' }
261+ { inlinePanel ? 'Close inline panel' : 'Open inline panel' }
200262 </ p >
263+ { diff ? (
264+ < >
265+ < p
266+ onClick = { acceptDiff }
267+ className = "text-green-600 cursor-pointer hover:text-green-800"
268+ >
269+ Accept diff
270+ </ p >
271+ < p
272+ onClick = { rejectDiff }
273+ className = "text-red-600 cursor-pointer hover:text-red-800"
274+ >
275+ Reject diff
276+ </ p >
277+ </ >
278+ ) : (
279+ < >
280+ < p
281+ onClick = { showDiff }
282+ className = "text-blue-500 cursor-pointer hover:text-blue-700"
283+ >
284+ Start diff
285+ </ p >
286+ < p
287+ onClick = { loadDemoContent }
288+ className = "text-blue-500 cursor-pointer hover:text-blue-700"
289+ >
290+ Load demo diff
291+ </ p >
292+ </ >
293+ ) }
201294 </ div >
202295 { commandRanCount > 0 && (
203296 < span className = "text-gray-400" >
0 commit comments