Skip to content

Commit 06c2186

Browse files
committed
tabs
1 parent a7cb01a commit 06c2186

File tree

2 files changed

+441
-9
lines changed

2 files changed

+441
-9
lines changed

src/dashboard/Data/Playground/Playground.react.js

Lines changed: 295 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,19 +176,56 @@ export default function Playground() {
176176
const [isResizing, setIsResizing] = useState(false);
177177
const [isAtBottom, setIsAtBottom] = useState(true); // Track if user is at bottom of console
178178
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);
179189

180190
const section = 'Core';
181191
const subsection = 'JS Console';
182192
const localKey = 'parse-dashboard-playground-code';
193+
const tabsKey = 'parse-dashboard-playground-tabs';
194+
const activeTabKey = 'parse-dashboard-playground-active-tab';
183195
const historyKey = 'parse-dashboard-playground-history';
184196
const heightKey = 'parse-dashboard-playground-height';
185197

186-
// Load saved code and history on mount
198+
// Load saved code, tabs, and history on mount
187199
useEffect(() => {
188200
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
189226
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 }]);
192229
}
193230

194231
const savedHistory = window.localStorage.getItem(historyKey);
@@ -212,7 +249,144 @@ export default function Playground() {
212249
}
213250
}
214251
}
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]);
216390

217391
// Handle mouse down on resize handle
218392
const handleResizeStart = useCallback((e) => {
@@ -416,6 +590,25 @@ export default function Playground() {
416590
return;
417591
}
418592

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+
419612
const restoreConsole = createConsoleOverride();
420613
setRunning(true);
421614
setResults([]);
@@ -455,7 +648,7 @@ export default function Playground() {
455648
restoreConsole();
456649
setRunning(false);
457650
}
458-
}, [context, createConsoleOverride, running, history, historyKey]);
651+
}, [context, createConsoleOverride, running, history, historyKey, tabs, activeTabId, activeTab, tabsKey]);
459652

460653
// Save code function with debouncing
461654
const saveCode = useCallback(() => {
@@ -467,15 +660,28 @@ export default function Playground() {
467660
setSaving(true);
468661
const code = editorRef.current.value;
469662

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+
}
471677

472678
// Show brief feedback that save was successful
473679
setTimeout(() => setSaving(false), 1000);
474680
} catch (e) {
475681
console.error('Save error:', e);
476682
setSaving(false);
477683
}
478-
}, [localKey, saving]);
684+
}, [saving, tabs, activeTabId, tabsKey, localKey]);
479685

480686
// Clear console
481687
const clearConsole = useCallback(() => {
@@ -666,15 +872,95 @@ export default function Playground() {
666872
</BrowserMenu>
667873
);
668874

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+
669894
return (
670895
<Toolbar section={section} subsection={subsection}>
671896
{runButton}
672897
<div className={browserStyles.toolbarSeparator} />
673898
{editMenu}
899+
<div className={browserStyles.toolbarSeparator} />
900+
{tabMenu}
674901
</Toolbar>
675902
);
676903
};
677904

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+
678964
return (
679965
<div className={styles['playground-ctn']}>
680966
{renderToolbar()}
@@ -683,8 +969,9 @@ export default function Playground() {
683969
className={styles['editor-section']}
684970
style={{ height: `${editorHeight}%` }}
685971
>
972+
{renderTabs()}
686973
<CodeEditor
687-
defaultValue={DEFAULT_CODE_EDITOR_VALUE}
974+
defaultValue={activeTab?.code || DEFAULT_CODE_EDITOR_VALUE}
688975
ref={editorRef}
689976
fontSize={14}
690977
/>

0 commit comments

Comments
 (0)