@@ -12,22 +12,30 @@ import {
1212 importSelectedNodes ,
1313} from "~/utils/importNodes" ;
1414import { getLoggedInClient , getSupabaseContext } from "~/utils/supabaseContext" ;
15+ import {
16+ computeImportPreview ,
17+ type ImportPreviewData ,
18+ } from "~/utils/importPreview" ;
1519
1620type ImportNodesModalProps = {
1721 plugin : DiscourseGraphPlugin ;
1822 onClose : ( ) => void ;
1923} ;
2024
2125const ImportNodesContent = ( { plugin, onClose } : ImportNodesModalProps ) => {
22- const [ step , setStep ] = useState < "loading" | "select" | "importing" > (
23- "loading" ,
24- ) ;
26+ const [ step , setStep ] = useState <
27+ "loading" | "select" | "preview" | "importing"
28+ > ( "loading" ) ;
2529 const [ groupsWithNodes , setGroupsWithNodes ] = useState < GroupWithNodes [ ] > ( [ ] ) ;
2630 const [ isLoading , setIsLoading ] = useState ( true ) ;
2731 const [ importProgress , setImportProgress ] = useState ( {
2832 current : 0 ,
2933 total : 0 ,
3034 } ) ;
35+ const [ previewData , setPreviewData ] = useState < ImportPreviewData | null > (
36+ null ,
37+ ) ;
38+ const [ previewLoading , setPreviewLoading ] = useState ( false ) ;
3139
3240 const loadImportableNodes = useCallback ( async ( ) => {
3341 setIsLoading ( true ) ;
@@ -146,16 +154,42 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => {
146154 ) ;
147155 } ;
148156
149- const handleImport = async ( ) => {
150- const selectedNodes : ImportableNode [ ] = [ ] ;
157+ const getSelectedNodes = ( ) : ImportableNode [ ] => {
158+ const selected : ImportableNode [ ] = [ ] ;
151159 for ( const group of groupsWithNodes ) {
152160 for ( const node of group . nodes ) {
153161 if ( node . selected ) {
154- selectedNodes . push ( node ) ;
162+ selected . push ( node ) ;
155163 }
156164 }
157165 }
166+ return selected ;
167+ } ;
168+
169+ const handleNext = async ( ) => {
170+ const selectedNodes = getSelectedNodes ( ) ;
171+ if ( selectedNodes . length === 0 ) {
172+ new Notice ( "Please select at least one node to import" ) ;
173+ return ;
174+ }
175+
176+ setPreviewLoading ( true ) ;
177+ try {
178+ const preview = await computeImportPreview ( { plugin, selectedNodes } ) ;
179+ setPreviewData ( preview ) ;
180+ setStep ( "preview" ) ;
181+ } catch ( error ) {
182+ console . error ( "Error computing preview:" , error ) ;
183+ const errorMessage =
184+ error instanceof Error ? error . message : String ( error ) ;
185+ new Notice ( `Failed to compute preview: ${ errorMessage } ` , 5000 ) ;
186+ } finally {
187+ setPreviewLoading ( false ) ;
188+ }
189+ } ;
158190
191+ const handleImport = async ( ) => {
192+ const selectedNodes = getSelectedNodes ( ) ;
159193 if ( selectedNodes . length === 0 ) {
160194 new Notice ( "Please select at least one node to import" ) ;
161195 return ;
@@ -171,6 +205,14 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => {
171205 onProgress : ( current , total ) => {
172206 setImportProgress ( { current, total } ) ;
173207 } ,
208+ precomputedData : previewData
209+ ? {
210+ nodeKeys : previewData . nodeKeys ,
211+ keyToRid : previewData . keyToRid ,
212+ keyToRelationEndpointId : previewData . keyToRelationEndpointId ,
213+ relationInstancesBySpace : previewData . relationInstancesBySpace ,
214+ }
215+ : undefined ,
174216 } ) ;
175217
176218 if ( result . failed > 0 ) {
@@ -321,14 +363,143 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => {
321363 < button onClick = { onClose } className = "px-4 py-2" >
322364 Cancel
323365 </ button >
366+ < button
367+ onClick = { ( ) => {
368+ void handleNext ( ) ;
369+ } }
370+ className = "!bg-accent !text-on-accent rounded px-4 py-2"
371+ disabled = { selectedCount === 0 || previewLoading }
372+ >
373+ { previewLoading ? "Loading..." : `Next (${ selectedCount } )` }
374+ </ button >
375+ </ div >
376+ </ div >
377+ ) ;
378+ } ;
379+
380+ const renderPreviewStep = ( ) => {
381+ if ( ! previewData ) return null ;
382+
383+ const hasNewNodeTypes = previewData . newNodeTypeSchemas . length > 0 ;
384+ const hasNewRelationTypes = previewData . newRelationTypeSchemas . length > 0 ;
385+ const newTriplets = previewData . relationTriplets . filter (
386+ ( t ) => t . isNewTriplet ,
387+ ) ;
388+ const hasNewTriplets = newTriplets . length > 0 ;
389+ const hasAnyNew = hasNewNodeTypes || hasNewRelationTypes || hasNewTriplets ;
390+
391+ return (
392+ < div >
393+ < h3 className = "mb-2" > Import Preview</ h3 >
394+ < p className = "text-muted mb-4 text-sm" >
395+ Review what will be imported and created.
396+ </ p >
397+
398+ < div className = "max-h-96 overflow-y-auto" >
399+ { /* Summary section */ }
400+ < div className = "mb-4 rounded border p-3" >
401+ < div className = "mb-1 text-sm font-medium uppercase tracking-wide opacity-60" >
402+ Summary
403+ </ div >
404+ < div className = "flex gap-6 text-sm" >
405+ < div >
406+ < span className = "font-semibold" >
407+ { previewData . selectedNodeCount }
408+ </ span > { " " }
409+ node{ previewData . selectedNodeCount !== 1 ? "s" : "" }
410+ </ div >
411+ < div >
412+ < span className = "font-semibold" >
413+ { previewData . relationInstanceCount }
414+ </ span > { " " }
415+ relation{ previewData . relationInstanceCount !== 1 ? "s" : "" }
416+ </ div >
417+ </ div >
418+ </ div >
419+
420+ { /* New schemas section */ }
421+ { hasAnyNew && (
422+ < div className = "mb-4 rounded border p-3" >
423+ < div className = "mb-2 text-sm font-medium uppercase tracking-wide opacity-60" >
424+ New schemas to create
425+ </ div >
426+
427+ { hasNewNodeTypes && (
428+ < div className = "mb-2" >
429+ < div className = "mb-1 text-sm font-medium" > Node types</ div >
430+ < div className = "flex flex-wrap gap-1" >
431+ { previewData . newNodeTypeSchemas . map ( ( nt ) => (
432+ < span
433+ key = { nt . id }
434+ className = "bg-accent/15 text-accent rounded px-2 py-0.5 text-xs"
435+ >
436+ { nt . name }
437+ </ span >
438+ ) ) }
439+ </ div >
440+ </ div >
441+ ) }
442+
443+ { hasNewRelationTypes && (
444+ < div className = "mb-2" >
445+ < div className = "mb-1 text-sm font-medium" > Relation types</ div >
446+ < div className = "flex flex-wrap gap-1" >
447+ { previewData . newRelationTypeSchemas . map ( ( rt ) => (
448+ < span
449+ key = { rt . id }
450+ className = "bg-accent/15 text-accent rounded px-2 py-0.5 text-xs"
451+ >
452+ { rt . label }
453+ { rt . complement ? ` / ${ rt . complement } ` : "" }
454+ </ span >
455+ ) ) }
456+ </ div >
457+ </ div >
458+ ) }
459+
460+ { hasNewTriplets && (
461+ < div >
462+ < div className = "mb-1 text-sm font-medium" >
463+ Discourse relations
464+ </ div >
465+ < div className = "space-y-1" >
466+ { newTriplets . map ( ( t , i ) => (
467+ < div key = { i } className = "flex items-center gap-1 text-xs" >
468+ < span className = "rounded bg-secondary px-1.5 py-0.5" >
469+ { t . sourceNodeTypeName }
470+ </ span >
471+ < span className = "text-accent font-medium" >
472+ { t . relationTypeLabel }
473+ </ span >
474+ < span className = "rounded bg-secondary px-1.5 py-0.5" >
475+ { t . destNodeTypeName }
476+ </ span >
477+ </ div >
478+ ) ) }
479+ </ div >
480+ </ div >
481+ ) }
482+ </ div >
483+ ) }
484+
485+ { ! hasAnyNew && (
486+ < div className = "text-muted rounded border p-3 text-center text-sm" >
487+ No new schemas or relations will be created.
488+ </ div >
489+ ) }
490+ </ div >
491+
492+ < div className = "mt-6 flex justify-between" >
493+ < button onClick = { ( ) => setStep ( "select" ) } className = "px-4 py-2" >
494+ Back
495+ </ button >
324496 < button
325497 onClick = { ( ) => {
326498 void handleImport ( ) ;
327499 } }
328500 className = "!bg-accent !text-on-accent rounded px-4 py-2"
329- disabled = { selectedCount === 0 }
330501 >
331- Import ( { selectedCount } )
502+ Confirm Import
332503 </ button >
333504 </ div >
334505 </ div >
@@ -361,6 +532,8 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => {
361532 switch ( step ) {
362533 case "select" :
363534 return renderSelectStep ( ) ;
535+ case "preview" :
536+ return renderPreviewStep ( ) ;
364537 case "importing" :
365538 return renderImportingStep ( ) ;
366539 default :
0 commit comments