Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 161 additions & 24 deletions src/dashboard/Data/Playground/Playground.react.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -249,26 +248,24 @@ 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);

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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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(() => {
Expand Down
33 changes: 23 additions & 10 deletions src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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.');
}
}

Expand Down Expand Up @@ -461,13 +471,16 @@ export default class DashboardSettings extends DashboardView {
}
/>
</Fieldset>
{this.viewPreferencesManager && this.viewPreferencesManager.isServerConfigEnabled() && (
{this.viewPreferencesManager && this.scriptManager && this.viewPreferencesManager.isServerConfigEnabled() && (
<Fieldset legend="Settings Storage">
<div style={{ marginBottom: '20px', color: '#666', fontSize: '14px', textAlign: 'center' }}>
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.
</div>
<Field
label={
<Label
text="Storage Location"
description="Choose where your dashboard settings are stored and loaded from. Server storage allows sharing settings across devices and users, while Browser storage is local to this device."
description="Choose where your dashboard settings are stored and loaded from."
/>
}
input={
Expand All @@ -487,7 +500,7 @@ export default class DashboardSettings extends DashboardView {
label={
<Label
text="Migrate Settings to Server"
description="Migrates your current browser-stored dashboard settings to the server. This does not change your storage preference - use the switch above to select the server as storage location after migration. ⚠️ This overwrites existing server settings."
description="Migrates browser-stored settings to the server. ⚠️ This overwrites existing dashboard settings on the server."
/>
}
input={
Expand All @@ -503,7 +516,7 @@ export default class DashboardSettings extends DashboardView {
label={
<Label
text="Delete Settings from Browser"
description="Removes your dashboard settings from the browser's local storage. This action is irreversible. Make sure to migrate your settings to server and test them first."
description="Removes settings from browser storage. ⚠️ Migrate your settings to the server and test them first."
/>
}
input={
Expand Down
Loading
Loading