diff --git a/src/dashboard/Data/Playground/Playground.react.js b/src/dashboard/Data/Playground/Playground.react.js index 8ba356101..459cc684f 100644 --- a/src/dashboard/Data/Playground/Playground.react.js +++ b/src/dashboard/Data/Playground/Playground.react.js @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect, useContext, useCallback, useMemo } from 'react'; import ReactJson from 'react-json-view'; import Parse from 'parse'; +import { useBeforeUnload } from 'react-router-dom'; import CodeEditor from 'components/CodeEditor/CodeEditor.react'; import Toolbar from 'components/Toolbar/Toolbar.react'; @@ -176,11 +177,11 @@ export default function Playground() { const containerRef = useRef(null); // Tab management state + const initialTabId = useMemo(() => crypto.randomUUID(), []); const [tabs, setTabs] = useState([ - { id: 1, name: 'Tab 1', code: DEFAULT_CODE_EDITOR_VALUE } + { id: initialTabId, name: 'Tab 1', code: DEFAULT_CODE_EDITOR_VALUE } ]); - const [activeTabId, setActiveTabId] = useState(1); - const [nextTabId, setNextTabId] = useState(2); + const [activeTabId, setActiveTabId] = useState(initialTabId); const [renamingTabId, setRenamingTabId] = useState(null); const [renamingValue, setRenamingValue] = useState(''); const [savedTabs, setSavedTabs] = useState([]); // All saved tabs including closed ones @@ -235,8 +236,6 @@ export default function Playground() { if (tabsToOpen.length > 0) { setTabs(tabsToOpen); - const maxId = Math.max(...allScripts.map(tab => tab.id)); - setNextTabId(maxId + 1); // Set active tab to the first one setActiveTabId(tabsToOpen[0].id); @@ -249,8 +248,6 @@ export default function Playground() { const firstScript = { ...allScripts[0], order: 0 }; setTabs([firstScript]); setActiveTabId(firstScript.id); - const maxId = Math.max(...allScripts.map(tab => tab.id)); - setNextTabId(maxId + 1); // Save it as open await scriptManagerRef.current.openScript(context.applicationId, firstScript.id, 0); @@ -258,17 +255,17 @@ export default function Playground() { setSavedTabs(allScripts.filter(script => script.saved !== false)); } else { // Fallback to default tab if no scripts exist - setTabs([{ id: 1, name: 'Tab 1', code: DEFAULT_CODE_EDITOR_VALUE, order: 0 }]); - setActiveTabId(1); - setNextTabId(2); + const defaultTabId = crypto.randomUUID(); + setTabs([{ id: defaultTabId, name: 'Tab 1', code: DEFAULT_CODE_EDITOR_VALUE, order: 0 }]); + setActiveTabId(defaultTabId); } } } catch (error) { console.warn('Failed to load scripts via ScriptManager:', error); // Fallback to default tab if loading fails - setTabs([{ id: 1, name: 'Tab 1', code: DEFAULT_CODE_EDITOR_VALUE, order: 0 }]); - setActiveTabId(1); - setNextTabId(2); + const defaultTabId = crypto.randomUUID(); + setTabs([{ id: defaultTabId, name: 'Tab 1', code: DEFAULT_CODE_EDITOR_VALUE, order: 0 }]); + setActiveTabId(defaultTabId); } // Load other data from localStorage @@ -317,18 +314,19 @@ export default function Playground() { // Tab management functions const createNewTab = useCallback(() => { + const newTabId = crypto.randomUUID(); + const tabCount = tabs.length + 1; const newTab = { - id: nextTabId, - name: `Tab ${nextTabId}`, + id: newTabId, + name: `Tab ${tabCount}`, code: '', // Start with empty code instead of default value saved: false, // Mark as unsaved initially order: tabs.length // Assign order as the last position }; const updatedTabs = [...tabs, newTab]; setTabs(updatedTabs); - setActiveTabId(nextTabId); - setNextTabId(nextTabId + 1); - }, [tabs, nextTabId]); + setActiveTabId(newTabId); + }, [tabs]); const closeTab = useCallback(async (tabId) => { if (tabs.length <= 1) { @@ -591,11 +589,6 @@ export default function Playground() { setTabs(updatedTabs); setActiveTabId(savedTab.id); - // Update nextTabId if necessary - if (savedTab.id >= nextTabId) { - setNextTabId(savedTab.id + 1); - } - // Save the open state through ScriptManager if (scriptManagerRef.current && context?.applicationId) { try { @@ -604,7 +597,151 @@ export default function Playground() { console.error('Failed to open script:', error); } } - }, [tabs, nextTabId, switchTab, context?.applicationId]); + }, [tabs, switchTab, context?.applicationId]); + + // Navigation confirmation for unsaved changes + useBeforeUnload( + useCallback( + (event) => { + // Check for unsaved changes across all tabs + let hasChanges = false; + + for (const tab of tabs) { + // Check if tab is marked as unsaved (like legacy scripts) + if (tab.saved === false) { + hasChanges = true; + break; + } + + // Get current content for the tab + let currentContent = ''; + if (tab.id === activeTabId && editorRef.current) { + // For active tab, get content from editor + currentContent = editorRef.current.value; + } else { + // For inactive tabs, use stored code + currentContent = tab.code; + } + + // Find the saved version of this tab + const savedTab = savedTabs.find(saved => saved.id === tab.id); + + if (!savedTab) { + // If tab was never saved, it has unsaved changes if it has any content + if (currentContent.trim() !== '') { + hasChanges = true; + break; + } + } else { + // Compare current content with saved content + if (currentContent !== savedTab.code) { + hasChanges = true; + break; + } + } + } + + if (hasChanges) { + const message = 'You have unsaved changes in your playground tabs. Are you sure you want to leave?'; + event.preventDefault(); + event.returnValue = message; + return message; + } + }, + [tabs, activeTabId, savedTabs] + ) + ); + + // Handle navigation confirmation for internal route changes + useEffect(() => { + const checkForUnsavedChanges = () => { + // Check for unsaved changes across all tabs + for (const tab of tabs) { + // Check if tab is marked as unsaved (like legacy scripts) + if (tab.saved === false) { + return true; + } + + // Get current content for the tab + let currentContent = ''; + if (tab.id === activeTabId && editorRef.current) { + // For active tab, get content from editor + currentContent = editorRef.current.value; + } else { + // For inactive tabs, use stored code + currentContent = tab.code; + } + + // Find the saved version of this tab + const savedTab = savedTabs.find(saved => saved.id === tab.id); + + if (!savedTab) { + // If tab was never saved, it has unsaved changes if it has any content + if (currentContent.trim() !== '') { + return true; + } + } else { + // Compare current content with saved content + if (currentContent !== savedTab.code) { + return true; + } + } + } + return false; + }; + + const handleLinkClick = (event) => { + if (event.defaultPrevented) { + return; + } + if (event.button !== 0) { + return; + } + if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) { + return; + } + + const anchor = event.target.closest('a[href]'); + if (!anchor || anchor.target === '_blank') { + return; + } + + const href = anchor.getAttribute('href'); + if (!href || href === '#') { + return; + } + + // Check if it's an internal navigation (starts with / or #) + if (href.startsWith('/') || href.startsWith('#')) { + if (checkForUnsavedChanges()) { + const message = 'You have unsaved changes in your playground tabs. Are you sure you want to leave?'; + if (!window.confirm(message)) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + }; + + const handlePopState = () => { + if (checkForUnsavedChanges()) { + const message = 'You have unsaved changes in your playground tabs. Are you sure you want to leave?'; + if (!window.confirm(message)) { + window.history.go(1); + } + } + }; + + // Add event listeners + document.addEventListener('click', handleLinkClick, true); + window.addEventListener('popstate', handlePopState); + + // Cleanup event listeners + return () => { + document.removeEventListener('click', handleLinkClick, true); + window.removeEventListener('popstate', handlePopState); + }; + }, [tabs, activeTabId, savedTabs]); // Focus input when starting to rename useEffect(() => { diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js index 4e2cd7b0f..c7d8f11aa 100644 --- a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js +++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js @@ -18,6 +18,7 @@ import Notification from 'dashboard/Data/Browser/Notification.react'; import * as ColumnPreferences from 'lib/ColumnPreferences'; import * as ClassPreferences from 'lib/ClassPreferences'; import ViewPreferencesManager from 'lib/ViewPreferencesManager'; +import ScriptManager from 'lib/ScriptManager'; import bcrypt from 'bcryptjs'; import * as OTPAuth from 'otpauth'; import QRCode from 'qrcode'; @@ -28,6 +29,7 @@ export default class DashboardSettings extends DashboardView { this.section = 'App Settings'; this.subsection = 'Dashboard Configuration'; this.viewPreferencesManager = null; + this.scriptManager = null; this.state = { createUserInput: false, @@ -57,12 +59,13 @@ export default class DashboardSettings extends DashboardView { } componentDidMount() { - this.initializeViewPreferencesManager(); + this.initializeManagers(); } - initializeViewPreferencesManager() { + initializeManagers() { if (this.context) { this.viewPreferencesManager = new ViewPreferencesManager(this.context); + this.scriptManager = new ScriptManager(this.context); this.loadStoragePreference(); } } @@ -123,11 +126,18 @@ export default class DashboardSettings extends DashboardView { return; } - const success = this.viewPreferencesManager.deleteFromBrowser(this.context.applicationId); - if (success) { - this.showNote('Successfully deleted views from browser storage.'); + if (!this.scriptManager) { + this.showNote('ScriptManager not initialized'); + return; + } + + const viewsSuccess = this.viewPreferencesManager.deleteFromBrowser(this.context.applicationId); + const scriptsSuccess = this.scriptManager.deleteFromBrowser(this.context.applicationId); + + if (viewsSuccess && scriptsSuccess) { + this.showNote('Successfully deleted dashboard settings from browser storage.'); } else { - this.showNote('Failed to delete views from browser storage.'); + this.showNote('Failed to delete all dashboard settings from browser storage.'); } } @@ -461,13 +471,16 @@ export default class DashboardSettings extends DashboardView { } /> - {this.viewPreferencesManager && this.viewPreferencesManager.isServerConfigEnabled() && ( + {this.viewPreferencesManager && this.scriptManager && this.viewPreferencesManager.isServerConfigEnabled() && (
+
+ Storing dashboard settings on the server rather than locally in the browser storage makes the settings available across devices and browsers. It also prevents them from getting lost when resetting the browser website data. Settings that can be stored on the server are currently Views and JS Console scripts. +
} input={ @@ -487,7 +500,7 @@ export default class DashboardSettings extends DashboardView { label={