1
1
import React , { useState , useRef , useEffect , useContext , useCallback , useMemo } from 'react' ;
2
2
import ReactJson from 'react-json-view' ;
3
3
import Parse from 'parse' ;
4
+ import { useBeforeUnload } from 'react-router-dom' ;
4
5
5
6
import CodeEditor from 'components/CodeEditor/CodeEditor.react' ;
6
7
import Toolbar from 'components/Toolbar/Toolbar.react' ;
@@ -176,11 +177,11 @@ export default function Playground() {
176
177
const containerRef = useRef ( null ) ;
177
178
178
179
// Tab management state
180
+ const initialTabId = useMemo ( ( ) => crypto . randomUUID ( ) , [ ] ) ;
179
181
const [ tabs , setTabs ] = useState ( [
180
- { id : 1 , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE }
182
+ { id : initialTabId , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE }
181
183
] ) ;
182
- const [ activeTabId , setActiveTabId ] = useState ( 1 ) ;
183
- const [ nextTabId , setNextTabId ] = useState ( 2 ) ;
184
+ const [ activeTabId , setActiveTabId ] = useState ( initialTabId ) ;
184
185
const [ renamingTabId , setRenamingTabId ] = useState ( null ) ;
185
186
const [ renamingValue , setRenamingValue ] = useState ( '' ) ;
186
187
const [ savedTabs , setSavedTabs ] = useState ( [ ] ) ; // All saved tabs including closed ones
@@ -235,8 +236,6 @@ export default function Playground() {
235
236
236
237
if ( tabsToOpen . length > 0 ) {
237
238
setTabs ( tabsToOpen ) ;
238
- const maxId = Math . max ( ...allScripts . map ( tab => tab . id ) ) ;
239
- setNextTabId ( maxId + 1 ) ;
240
239
241
240
// Set active tab to the first one
242
241
setActiveTabId ( tabsToOpen [ 0 ] . id ) ;
@@ -249,26 +248,24 @@ export default function Playground() {
249
248
const firstScript = { ...allScripts [ 0 ] , order : 0 } ;
250
249
setTabs ( [ firstScript ] ) ;
251
250
setActiveTabId ( firstScript . id ) ;
252
- const maxId = Math . max ( ...allScripts . map ( tab => tab . id ) ) ;
253
- setNextTabId ( maxId + 1 ) ;
254
251
255
252
// Save it as open
256
253
await scriptManagerRef . current . openScript ( context . applicationId , firstScript . id , 0 ) ;
257
254
258
255
setSavedTabs ( allScripts . filter ( script => script . saved !== false ) ) ;
259
256
} else {
260
257
// Fallback to default tab if no scripts exist
261
- setTabs ( [ { id : 1 , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE , order : 0 } ] ) ;
262
- setActiveTabId ( 1 ) ;
263
- setNextTabId ( 2 ) ;
258
+ const defaultTabId = crypto . randomUUID ( ) ;
259
+ setTabs ( [ { id : defaultTabId , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE , order : 0 } ] ) ;
260
+ setActiveTabId ( defaultTabId ) ;
264
261
}
265
262
}
266
263
} catch ( error ) {
267
264
console . warn ( 'Failed to load scripts via ScriptManager:' , error ) ;
268
265
// Fallback to default tab if loading fails
269
- setTabs ( [ { id : 1 , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE , order : 0 } ] ) ;
270
- setActiveTabId ( 1 ) ;
271
- setNextTabId ( 2 ) ;
266
+ const defaultTabId = crypto . randomUUID ( ) ;
267
+ setTabs ( [ { id : defaultTabId , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE , order : 0 } ] ) ;
268
+ setActiveTabId ( defaultTabId ) ;
272
269
}
273
270
274
271
// Load other data from localStorage
@@ -317,18 +314,19 @@ export default function Playground() {
317
314
318
315
// Tab management functions
319
316
const createNewTab = useCallback ( ( ) => {
317
+ const newTabId = crypto . randomUUID ( ) ;
318
+ const tabCount = tabs . length + 1 ;
320
319
const newTab = {
321
- id : nextTabId ,
322
- name : `Tab ${ nextTabId } ` ,
320
+ id : newTabId ,
321
+ name : `Tab ${ tabCount } ` ,
323
322
code : '' , // Start with empty code instead of default value
324
323
saved : false , // Mark as unsaved initially
325
324
order : tabs . length // Assign order as the last position
326
325
} ;
327
326
const updatedTabs = [ ...tabs , newTab ] ;
328
327
setTabs ( updatedTabs ) ;
329
- setActiveTabId ( nextTabId ) ;
330
- setNextTabId ( nextTabId + 1 ) ;
331
- } , [ tabs , nextTabId ] ) ;
328
+ setActiveTabId ( newTabId ) ;
329
+ } , [ tabs ] ) ;
332
330
333
331
const closeTab = useCallback ( async ( tabId ) => {
334
332
if ( tabs . length <= 1 ) {
@@ -591,11 +589,6 @@ export default function Playground() {
591
589
setTabs ( updatedTabs ) ;
592
590
setActiveTabId ( savedTab . id ) ;
593
591
594
- // Update nextTabId if necessary
595
- if ( savedTab . id >= nextTabId ) {
596
- setNextTabId ( savedTab . id + 1 ) ;
597
- }
598
-
599
592
// Save the open state through ScriptManager
600
593
if ( scriptManagerRef . current && context ?. applicationId ) {
601
594
try {
@@ -604,7 +597,151 @@ export default function Playground() {
604
597
console . error ( 'Failed to open script:' , error ) ;
605
598
}
606
599
}
607
- } , [ tabs , nextTabId , switchTab , context ?. applicationId ] ) ;
600
+ } , [ tabs , switchTab , context ?. applicationId ] ) ;
601
+
602
+ // Navigation confirmation for unsaved changes
603
+ useBeforeUnload (
604
+ useCallback (
605
+ ( event ) => {
606
+ // Check for unsaved changes across all tabs
607
+ let hasChanges = false ;
608
+
609
+ for ( const tab of tabs ) {
610
+ // Check if tab is marked as unsaved (like legacy scripts)
611
+ if ( tab . saved === false ) {
612
+ hasChanges = true ;
613
+ break ;
614
+ }
615
+
616
+ // Get current content for the tab
617
+ let currentContent = '' ;
618
+ if ( tab . id === activeTabId && editorRef . current ) {
619
+ // For active tab, get content from editor
620
+ currentContent = editorRef . current . value ;
621
+ } else {
622
+ // For inactive tabs, use stored code
623
+ currentContent = tab . code ;
624
+ }
625
+
626
+ // Find the saved version of this tab
627
+ const savedTab = savedTabs . find ( saved => saved . id === tab . id ) ;
628
+
629
+ if ( ! savedTab ) {
630
+ // If tab was never saved, it has unsaved changes if it has any content
631
+ if ( currentContent . trim ( ) !== '' ) {
632
+ hasChanges = true ;
633
+ break ;
634
+ }
635
+ } else {
636
+ // Compare current content with saved content
637
+ if ( currentContent !== savedTab . code ) {
638
+ hasChanges = true ;
639
+ break ;
640
+ }
641
+ }
642
+ }
643
+
644
+ if ( hasChanges ) {
645
+ const message = 'You have unsaved changes in your playground tabs. Are you sure you want to leave?' ;
646
+ event . preventDefault ( ) ;
647
+ event . returnValue = message ;
648
+ return message ;
649
+ }
650
+ } ,
651
+ [ tabs , activeTabId , savedTabs ]
652
+ )
653
+ ) ;
654
+
655
+ // Handle navigation confirmation for internal route changes
656
+ useEffect ( ( ) => {
657
+ const checkForUnsavedChanges = ( ) => {
658
+ // Check for unsaved changes across all tabs
659
+ for ( const tab of tabs ) {
660
+ // Check if tab is marked as unsaved (like legacy scripts)
661
+ if ( tab . saved === false ) {
662
+ return true ;
663
+ }
664
+
665
+ // Get current content for the tab
666
+ let currentContent = '' ;
667
+ if ( tab . id === activeTabId && editorRef . current ) {
668
+ // For active tab, get content from editor
669
+ currentContent = editorRef . current . value ;
670
+ } else {
671
+ // For inactive tabs, use stored code
672
+ currentContent = tab . code ;
673
+ }
674
+
675
+ // Find the saved version of this tab
676
+ const savedTab = savedTabs . find ( saved => saved . id === tab . id ) ;
677
+
678
+ if ( ! savedTab ) {
679
+ // If tab was never saved, it has unsaved changes if it has any content
680
+ if ( currentContent . trim ( ) !== '' ) {
681
+ return true ;
682
+ }
683
+ } else {
684
+ // Compare current content with saved content
685
+ if ( currentContent !== savedTab . code ) {
686
+ return true ;
687
+ }
688
+ }
689
+ }
690
+ return false ;
691
+ } ;
692
+
693
+ const handleLinkClick = ( event ) => {
694
+ if ( event . defaultPrevented ) {
695
+ return ;
696
+ }
697
+ if ( event . button !== 0 ) {
698
+ return ;
699
+ }
700
+ if ( event . metaKey || event . altKey || event . ctrlKey || event . shiftKey ) {
701
+ return ;
702
+ }
703
+
704
+ const anchor = event . target . closest ( 'a[href]' ) ;
705
+ if ( ! anchor || anchor . target === '_blank' ) {
706
+ return ;
707
+ }
708
+
709
+ const href = anchor . getAttribute ( 'href' ) ;
710
+ if ( ! href || href === '#' ) {
711
+ return ;
712
+ }
713
+
714
+ // Check if it's an internal navigation (starts with / or #)
715
+ if ( href . startsWith ( '/' ) || href . startsWith ( '#' ) ) {
716
+ if ( checkForUnsavedChanges ( ) ) {
717
+ const message = 'You have unsaved changes in your playground tabs. Are you sure you want to leave?' ;
718
+ if ( ! window . confirm ( message ) ) {
719
+ event . preventDefault ( ) ;
720
+ event . stopPropagation ( ) ;
721
+ }
722
+ }
723
+ }
724
+ } ;
725
+
726
+ const handlePopState = ( ) => {
727
+ if ( checkForUnsavedChanges ( ) ) {
728
+ const message = 'You have unsaved changes in your playground tabs. Are you sure you want to leave?' ;
729
+ if ( ! window . confirm ( message ) ) {
730
+ window . history . go ( 1 ) ;
731
+ }
732
+ }
733
+ } ;
734
+
735
+ // Add event listeners
736
+ document . addEventListener ( 'click' , handleLinkClick , true ) ;
737
+ window . addEventListener ( 'popstate' , handlePopState ) ;
738
+
739
+ // Cleanup event listeners
740
+ return ( ) => {
741
+ document . removeEventListener ( 'click' , handleLinkClick , true ) ;
742
+ window . removeEventListener ( 'popstate' , handlePopState ) ;
743
+ } ;
744
+ } , [ tabs , activeTabId , savedTabs ] ) ;
608
745
609
746
// Focus input when starting to rename
610
747
useEffect ( ( ) => {
0 commit comments