@@ -176,19 +176,56 @@ export default function Playground() {
176
176
const [ isResizing , setIsResizing ] = useState ( false ) ;
177
177
const [ isAtBottom , setIsAtBottom ] = useState ( true ) ; // Track if user is at bottom of console
178
178
const containerRef = useRef ( null ) ;
179
+
180
+ // Tab management state
181
+ const [ tabs , setTabs ] = useState ( [
182
+ { id : 1 , name : 'Tab 1' , code : DEFAULT_CODE_EDITOR_VALUE }
183
+ ] ) ;
184
+ const [ activeTabId , setActiveTabId ] = useState ( 1 ) ;
185
+ const [ nextTabId , setNextTabId ] = useState ( 2 ) ;
186
+ const [ renamingTabId , setRenamingTabId ] = useState ( null ) ;
187
+ const [ renamingValue , setRenamingValue ] = useState ( '' ) ;
188
+ const renamingInputRef = useRef ( null ) ;
179
189
180
190
const section = 'Core' ;
181
191
const subsection = 'JS Console' ;
182
192
const localKey = 'parse-dashboard-playground-code' ;
193
+ const tabsKey = 'parse-dashboard-playground-tabs' ;
194
+ const activeTabKey = 'parse-dashboard-playground-active-tab' ;
183
195
const historyKey = 'parse-dashboard-playground-history' ;
184
196
const heightKey = 'parse-dashboard-playground-height' ;
185
197
186
- // Load saved code and history on mount
198
+ // Load saved code, tabs, and history on mount
187
199
useEffect ( ( ) => {
188
200
if ( window . localStorage ) {
201
+ // Load tabs
202
+ const savedTabs = window . localStorage . getItem ( tabsKey ) ;
203
+ const savedActiveTabId = window . localStorage . getItem ( activeTabKey ) ;
204
+
205
+ if ( savedTabs ) {
206
+ try {
207
+ const parsedTabs = JSON . parse ( savedTabs ) ;
208
+ if ( parsedTabs . length > 0 ) {
209
+ setTabs ( parsedTabs ) ;
210
+ const maxId = Math . max ( ...parsedTabs . map ( tab => tab . id ) ) ;
211
+ setNextTabId ( maxId + 1 ) ;
212
+
213
+ if ( savedActiveTabId ) {
214
+ const activeId = parseInt ( savedActiveTabId ) ;
215
+ if ( parsedTabs . find ( tab => tab . id === activeId ) ) {
216
+ setActiveTabId ( activeId ) ;
217
+ }
218
+ }
219
+ }
220
+ } catch ( e ) {
221
+ console . warn ( 'Failed to load tabs:' , e ) ;
222
+ }
223
+ }
224
+
225
+ // Load legacy single code if no tabs exist
189
226
const initialCode = window . localStorage . getItem ( localKey ) ;
190
- if ( initialCode && editorRef . current ) {
191
- editorRef . current . value = initialCode ;
227
+ if ( initialCode && ! savedTabs ) {
228
+ setTabs ( [ { id : 1 , name : 'Tab 1' , code : initialCode } ] ) ;
192
229
}
193
230
194
231
const savedHistory = window . localStorage . getItem ( historyKey ) ;
@@ -212,7 +249,144 @@ export default function Playground() {
212
249
}
213
250
}
214
251
}
215
- } , [ localKey , historyKey , heightKey ] ) ;
252
+ } , [ localKey , tabsKey , activeTabKey , historyKey , heightKey ] ) ;
253
+
254
+ // Get current active tab
255
+ const activeTab = tabs . find ( tab => tab . id === activeTabId ) || tabs [ 0 ] ;
256
+
257
+ // Update editor when active tab changes
258
+ useEffect ( ( ) => {
259
+ if ( editorRef . current && activeTab ) {
260
+ editorRef . current . value = activeTab . code ;
261
+ }
262
+ } , [ activeTabId , activeTab ] ) ;
263
+
264
+ // Tab management functions
265
+ const createNewTab = useCallback ( ( ) => {
266
+ const newTab = {
267
+ id : nextTabId ,
268
+ name : `Tab ${ nextTabId } ` ,
269
+ code : DEFAULT_CODE_EDITOR_VALUE
270
+ } ;
271
+ const updatedTabs = [ ...tabs , newTab ] ;
272
+ setTabs ( updatedTabs ) ;
273
+ setActiveTabId ( nextTabId ) ;
274
+ setNextTabId ( nextTabId + 1 ) ;
275
+
276
+ // Save to localStorage
277
+ if ( window . localStorage ) {
278
+ try {
279
+ window . localStorage . setItem ( tabsKey , JSON . stringify ( updatedTabs ) ) ;
280
+ window . localStorage . setItem ( activeTabKey , nextTabId . toString ( ) ) ;
281
+ } catch ( e ) {
282
+ console . warn ( 'Failed to save tabs:' , e ) ;
283
+ }
284
+ }
285
+ } , [ tabs , nextTabId , tabsKey , activeTabKey ] ) ;
286
+
287
+ const closeTab = useCallback ( ( tabId ) => {
288
+ if ( tabs . length <= 1 ) {
289
+ return ; // Don't close the last tab
290
+ }
291
+
292
+ const updatedTabs = tabs . filter ( tab => tab . id !== tabId ) ;
293
+ setTabs ( updatedTabs ) ;
294
+
295
+ // If closing active tab, switch to another tab
296
+ if ( tabId === activeTabId ) {
297
+ const newActiveTab = updatedTabs [ 0 ] ;
298
+ setActiveTabId ( newActiveTab . id ) ;
299
+ }
300
+
301
+ // Save to localStorage
302
+ if ( window . localStorage ) {
303
+ try {
304
+ window . localStorage . setItem ( tabsKey , JSON . stringify ( updatedTabs ) ) ;
305
+ if ( tabId === activeTabId ) {
306
+ window . localStorage . setItem ( activeTabKey , updatedTabs [ 0 ] . id . toString ( ) ) ;
307
+ }
308
+ } catch ( e ) {
309
+ console . warn ( 'Failed to save tabs:' , e ) ;
310
+ }
311
+ }
312
+ } , [ tabs , activeTabId , tabsKey , activeTabKey ] ) ;
313
+
314
+ const switchTab = useCallback ( ( tabId ) => {
315
+ // Save current tab's code before switching
316
+ if ( editorRef . current && activeTab ) {
317
+ const updatedTabs = tabs . map ( tab =>
318
+ tab . id === activeTabId
319
+ ? { ...tab , code : editorRef . current . value }
320
+ : tab
321
+ ) ;
322
+ setTabs ( updatedTabs ) ;
323
+
324
+ // Save to localStorage
325
+ if ( window . localStorage ) {
326
+ try {
327
+ window . localStorage . setItem ( tabsKey , JSON . stringify ( updatedTabs ) ) ;
328
+ } catch ( e ) {
329
+ console . warn ( 'Failed to save tabs:' , e ) ;
330
+ }
331
+ }
332
+ }
333
+
334
+ setActiveTabId ( tabId ) ;
335
+
336
+ // Save active tab to localStorage
337
+ if ( window . localStorage ) {
338
+ try {
339
+ window . localStorage . setItem ( activeTabKey , tabId . toString ( ) ) ;
340
+ } catch ( e ) {
341
+ console . warn ( 'Failed to save active tab:' , e ) ;
342
+ }
343
+ }
344
+ } , [ tabs , activeTabId , activeTab , tabsKey , activeTabKey ] ) ;
345
+
346
+ const renameTab = useCallback ( ( tabId , newName ) => {
347
+ if ( ! newName . trim ( ) ) {
348
+ return ;
349
+ }
350
+
351
+ const updatedTabs = tabs . map ( tab =>
352
+ tab . id === tabId ? { ...tab , name : newName . trim ( ) } : tab
353
+ ) ;
354
+ setTabs ( updatedTabs ) ;
355
+
356
+ // Save to localStorage
357
+ if ( window . localStorage ) {
358
+ try {
359
+ window . localStorage . setItem ( tabsKey , JSON . stringify ( updatedTabs ) ) ;
360
+ } catch ( e ) {
361
+ console . warn ( 'Failed to save tabs:' , e ) ;
362
+ }
363
+ }
364
+ } , [ tabs , tabsKey ] ) ;
365
+
366
+ const startRenaming = useCallback ( ( tabId , currentName ) => {
367
+ setRenamingTabId ( tabId ) ;
368
+ setRenamingValue ( currentName ) ;
369
+ } , [ ] ) ;
370
+
371
+ const cancelRenaming = useCallback ( ( ) => {
372
+ setRenamingTabId ( null ) ;
373
+ setRenamingValue ( '' ) ;
374
+ } , [ ] ) ;
375
+
376
+ const confirmRenaming = useCallback ( ( ) => {
377
+ if ( renamingTabId && renamingValue . trim ( ) ) {
378
+ renameTab ( renamingTabId , renamingValue ) ;
379
+ }
380
+ cancelRenaming ( ) ;
381
+ } , [ renamingTabId , renamingValue , renameTab , cancelRenaming ] ) ;
382
+
383
+ // Focus input when starting to rename
384
+ useEffect ( ( ) => {
385
+ if ( renamingTabId && renamingInputRef . current ) {
386
+ renamingInputRef . current . focus ( ) ;
387
+ renamingInputRef . current . select ( ) ;
388
+ }
389
+ } , [ renamingTabId ] ) ;
216
390
217
391
// Handle mouse down on resize handle
218
392
const handleResizeStart = useCallback ( ( e ) => {
@@ -416,6 +590,25 @@ export default function Playground() {
416
590
return ;
417
591
}
418
592
593
+ // Save current tab's code before running
594
+ if ( activeTab ) {
595
+ const updatedTabs = tabs . map ( tab =>
596
+ tab . id === activeTabId
597
+ ? { ...tab , code : code }
598
+ : tab
599
+ ) ;
600
+ setTabs ( updatedTabs ) ;
601
+
602
+ // Save to localStorage
603
+ if ( window . localStorage ) {
604
+ try {
605
+ window . localStorage . setItem ( tabsKey , JSON . stringify ( updatedTabs ) ) ;
606
+ } catch ( e ) {
607
+ console . warn ( 'Failed to save tabs:' , e ) ;
608
+ }
609
+ }
610
+ }
611
+
419
612
const restoreConsole = createConsoleOverride ( ) ;
420
613
setRunning ( true ) ;
421
614
setResults ( [ ] ) ;
@@ -455,7 +648,7 @@ export default function Playground() {
455
648
restoreConsole ( ) ;
456
649
setRunning ( false ) ;
457
650
}
458
- } , [ context , createConsoleOverride , running , history , historyKey ] ) ;
651
+ } , [ context , createConsoleOverride , running , history , historyKey , tabs , activeTabId , activeTab , tabsKey ] ) ;
459
652
460
653
// Save code function with debouncing
461
654
const saveCode = useCallback ( ( ) => {
@@ -467,15 +660,28 @@ export default function Playground() {
467
660
setSaving ( true ) ;
468
661
const code = editorRef . current . value ;
469
662
470
- window . localStorage . setItem ( localKey , code ) ;
663
+ // Update current tab's code
664
+ const updatedTabs = tabs . map ( tab =>
665
+ tab . id === activeTabId
666
+ ? { ...tab , code : code }
667
+ : tab
668
+ ) ;
669
+ setTabs ( updatedTabs ) ;
670
+
671
+ // Save tabs to localStorage
672
+ if ( window . localStorage ) {
673
+ window . localStorage . setItem ( tabsKey , JSON . stringify ( updatedTabs ) ) ;
674
+ // Also save to legacy key for backward compatibility
675
+ window . localStorage . setItem ( localKey , code ) ;
676
+ }
471
677
472
678
// Show brief feedback that save was successful
473
679
setTimeout ( ( ) => setSaving ( false ) , 1000 ) ;
474
680
} catch ( e ) {
475
681
console . error ( 'Save error:' , e ) ;
476
682
setSaving ( false ) ;
477
683
}
478
- } , [ localKey , saving ] ) ;
684
+ } , [ saving , tabs , activeTabId , tabsKey , localKey ] ) ;
479
685
480
686
// Clear console
481
687
const clearConsole = useCallback ( ( ) => {
@@ -666,15 +872,95 @@ export default function Playground() {
666
872
</ BrowserMenu >
667
873
) ;
668
874
875
+ const tabMenu = (
876
+ < BrowserMenu title = "Tabs" icon = "window-solid" setCurrent = { ( ) => { } } >
877
+ < MenuItem
878
+ text = "New Tab"
879
+ onClick = { createNewTab }
880
+ />
881
+ < MenuItem
882
+ text = "Rename Tab"
883
+ onClick = { ( ) => startRenaming ( activeTabId , activeTab ?. name || '' ) }
884
+ />
885
+ { tabs . length > 1 && (
886
+ < MenuItem
887
+ text = "Close Tab"
888
+ onClick = { ( ) => closeTab ( activeTabId ) }
889
+ />
890
+ ) }
891
+ </ BrowserMenu >
892
+ ) ;
893
+
669
894
return (
670
895
< Toolbar section = { section } subsection = { subsection } >
671
896
{ runButton }
672
897
< div className = { browserStyles . toolbarSeparator } />
673
898
{ editMenu }
899
+ < div className = { browserStyles . toolbarSeparator } />
900
+ { tabMenu }
674
901
</ Toolbar >
675
902
) ;
676
903
} ;
677
904
905
+ const renderTabs = ( ) => {
906
+ return (
907
+ < div className = { styles [ 'tab-bar' ] } >
908
+ < div className = { styles [ 'tab-container' ] } >
909
+ { tabs . map ( tab => (
910
+ < div
911
+ key = { tab . id }
912
+ className = { `${ styles [ 'tab' ] } ${ tab . id === activeTabId ? styles [ 'tab-active' ] : '' } ` }
913
+ onClick = { ( ) => switchTab ( tab . id ) }
914
+ >
915
+ { renamingTabId === tab . id ? (
916
+ < input
917
+ ref = { renamingInputRef }
918
+ type = "text"
919
+ value = { renamingValue }
920
+ onChange = { ( e ) => setRenamingValue ( e . target . value ) }
921
+ onBlur = { confirmRenaming }
922
+ onKeyDown = { ( e ) => {
923
+ if ( e . key === 'Enter' ) {
924
+ confirmRenaming ( ) ;
925
+ } else if ( e . key === 'Escape' ) {
926
+ cancelRenaming ( ) ;
927
+ }
928
+ } }
929
+ onClick = { ( e ) => e . stopPropagation ( ) }
930
+ className = { styles [ 'tab-rename-input' ] }
931
+ />
932
+ ) : (
933
+ < span
934
+ className = { styles [ 'tab-name' ] }
935
+ onDoubleClick = { ( e ) => {
936
+ e . stopPropagation ( ) ;
937
+ startRenaming ( tab . id , tab . name ) ;
938
+ } }
939
+ >
940
+ { tab . name }
941
+ </ span >
942
+ ) }
943
+ { tabs . length > 1 && (
944
+ < button
945
+ className = { styles [ 'tab-close' ] }
946
+ onClick = { ( e ) => {
947
+ e . stopPropagation ( ) ;
948
+ closeTab ( tab . id ) ;
949
+ } }
950
+ >
951
+ ×
952
+ </ button >
953
+ ) }
954
+ </ div >
955
+ ) ) }
956
+ < button className = { styles [ 'tab-new' ] } onClick = { createNewTab } >
957
+ +
958
+ </ button >
959
+ </ div >
960
+ </ div >
961
+ ) ;
962
+ } ;
963
+
678
964
return (
679
965
< div className = { styles [ 'playground-ctn' ] } >
680
966
{ renderToolbar ( ) }
@@ -683,8 +969,9 @@ export default function Playground() {
683
969
className = { styles [ 'editor-section' ] }
684
970
style = { { height : `${ editorHeight } %` } }
685
971
>
972
+ { renderTabs ( ) }
686
973
< CodeEditor
687
- defaultValue = { DEFAULT_CODE_EDITOR_VALUE }
974
+ defaultValue = { activeTab ?. code || DEFAULT_CODE_EDITOR_VALUE }
688
975
ref = { editorRef }
689
976
fontSize = { 14 }
690
977
/>
0 commit comments