refactor: reorganize settings — merge Notifications into Sessions, move device name to Display, remove view scope
This commit is contained in:
+5
-30
@@ -1593,36 +1593,23 @@ function applyDisplaySettings(ds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load grid view mode preference based on viewPreferenceScope in display settings.
|
* Load grid view mode preference from display settings (localStorage).
|
||||||
* When scope is 'local' (default), reads gridViewMode from display settings (localStorage).
|
|
||||||
* When scope is 'server', reads grid_view_mode from _serverSettings.
|
|
||||||
* Returns 'flat' as default.
|
* Returns 'flat' as default.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function loadGridViewMode() {
|
function loadGridViewMode() {
|
||||||
var ds = loadDisplaySettings();
|
var ds = loadDisplaySettings();
|
||||||
var scope = ds.viewPreferenceScope || 'local';
|
|
||||||
if (scope === 'server') {
|
|
||||||
return (_serverSettings && _serverSettings.grid_view_mode) || 'flat';
|
|
||||||
}
|
|
||||||
return ds.gridViewMode || 'flat';
|
return ds.gridViewMode || 'flat';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save grid view mode preference to the appropriate scope and update _gridViewMode.
|
* Save grid view mode preference to display settings (localStorage) and update _gridViewMode.
|
||||||
* When scope is 'local', saves gridViewMode to display settings (localStorage).
|
|
||||||
* When scope is 'server', patches the server setting via patchServerSetting.
|
|
||||||
* @param {string} mode - The grid view mode to save.
|
* @param {string} mode - The grid view mode to save.
|
||||||
*/
|
*/
|
||||||
function saveGridViewMode(mode) {
|
function saveGridViewMode(mode) {
|
||||||
var ds = loadDisplaySettings();
|
var ds = loadDisplaySettings();
|
||||||
var scope = ds.viewPreferenceScope || 'local';
|
ds.gridViewMode = mode;
|
||||||
if (scope === 'server') {
|
saveDisplaySettings(ds);
|
||||||
patchServerSetting('grid_view_mode', mode);
|
|
||||||
} else {
|
|
||||||
ds.gridViewMode = mode;
|
|
||||||
saveDisplaySettings(ds);
|
|
||||||
}
|
|
||||||
_gridViewMode = mode;
|
_gridViewMode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1693,8 +1680,6 @@ function openSettings() {
|
|||||||
if (gridColumnsEl) gridColumnsEl.value = String(settings.gridColumns);
|
if (gridColumnsEl) gridColumnsEl.value = String(settings.gridColumns);
|
||||||
const viewModeEl = $('setting-view-mode');
|
const viewModeEl = $('setting-view-mode');
|
||||||
if (viewModeEl) viewModeEl.value = loadGridViewMode();
|
if (viewModeEl) viewModeEl.value = loadGridViewMode();
|
||||||
const viewScopeEl = $('setting-view-scope');
|
|
||||||
if (viewScopeEl) viewScopeEl.value = settings.viewPreferenceScope || 'local';
|
|
||||||
|
|
||||||
// Populate Notifications tab from display settings
|
// Populate Notifications tab from display settings
|
||||||
const bellSoundEl = $('setting-bell-sound');
|
const bellSoundEl = $('setting-bell-sound');
|
||||||
@@ -2225,17 +2210,7 @@ function bindStaticEventListeners() {
|
|||||||
renderGrid(_currentSessions || []);
|
renderGrid(_currentSessions || []);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
on($('setting-view-scope'), 'change', function() {
|
|
||||||
var el = $('setting-view-scope');
|
|
||||||
if (!el) return;
|
|
||||||
var newScope = el.value;
|
|
||||||
var currentMode = _gridViewMode;
|
|
||||||
var ds = loadDisplaySettings();
|
|
||||||
ds.viewPreferenceScope = newScope;
|
|
||||||
saveDisplaySettings(ds);
|
|
||||||
// Migrate current mode to new scope
|
|
||||||
saveGridViewMode(currentMode);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sessions settings — bind change events for server-side persistence
|
// Sessions settings — bind change events for server-side persistence
|
||||||
on($('setting-default-session'), 'change', function() {
|
on($('setting-default-session'), 'change', function() {
|
||||||
|
|||||||
@@ -88,7 +88,6 @@
|
|||||||
<nav class="settings-tabs">
|
<nav class="settings-tabs">
|
||||||
<button class="settings-tab settings-tab--active" data-tab="display">Display</button>
|
<button class="settings-tab settings-tab--active" data-tab="display">Display</button>
|
||||||
<button class="settings-tab" data-tab="sessions">Sessions</button>
|
<button class="settings-tab" data-tab="sessions">Sessions</button>
|
||||||
<button class="settings-tab" data-tab="notifications">Notifications</button>
|
|
||||||
<button class="settings-tab" data-tab="new-session">Commands</button>
|
<button class="settings-tab" data-tab="new-session">Commands</button>
|
||||||
<button class="settings-tab" data-tab="devices">Multi-Device</button>
|
<button class="settings-tab" data-tab="devices">Multi-Device</button>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -124,6 +123,10 @@
|
|||||||
<option value="4">4</option>
|
<option value="4">4</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-field">
|
||||||
|
<label class="settings-label" for="setting-device-name">Device Name</label>
|
||||||
|
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-panel hidden" data-tab="sessions">
|
<div class="settings-panel hidden" data-tab="sessions">
|
||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
@@ -152,8 +155,6 @@
|
|||||||
<label class="settings-label" for="setting-auto-open">Auto-open created sessions</label>
|
<label class="settings-label" for="setting-auto-open">Auto-open created sessions</label>
|
||||||
<input type="checkbox" id="setting-auto-open" class="settings-checkbox" checked />
|
<input type="checkbox" id="setting-auto-open" class="settings-checkbox" checked />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="settings-panel hidden" data-tab="notifications">
|
|
||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label class="settings-label" for="setting-bell-sound">Bell Sound</label>
|
<label class="settings-label" for="setting-bell-sound">Bell Sound</label>
|
||||||
<input type="checkbox" id="setting-bell-sound" class="settings-checkbox" />
|
<input type="checkbox" id="setting-bell-sound" class="settings-checkbox" />
|
||||||
@@ -186,10 +187,6 @@
|
|||||||
<input type="checkbox" id="setting-multi-device-enabled" class="settings-checkbox" />
|
<input type="checkbox" id="setting-multi-device-enabled" class="settings-checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<div id="multi-device-fields">
|
<div id="multi-device-fields">
|
||||||
<div class="settings-field">
|
|
||||||
<label class="settings-label" for="setting-device-name">Device Name</label>
|
|
||||||
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
|
|
||||||
</div>
|
|
||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label class="settings-label" for="setting-view-mode">View Mode</label>
|
<label class="settings-label" for="setting-view-mode">View Mode</label>
|
||||||
<select id="setting-view-mode" class="settings-select">
|
<select id="setting-view-mode" class="settings-select">
|
||||||
@@ -198,13 +195,6 @@
|
|||||||
<option value="filtered">Filtered</option>
|
<option value="filtered">Filtered</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-field">
|
|
||||||
<label class="settings-label" for="setting-view-scope">View Scope</label>
|
|
||||||
<select id="setting-view-scope" class="settings-select">
|
|
||||||
<option value="local">Local</option>
|
|
||||||
<option value="server">Server</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="settings-field settings-field--column">
|
<div class="settings-field settings-field--column">
|
||||||
<label class="settings-label">Remote Instances</label>
|
<label class="settings-label">Remote Instances</label>
|
||||||
<div id="setting-remote-instances" class="settings-remote-list"></div>
|
<div id="setting-remote-instances" class="settings-remote-list"></div>
|
||||||
|
|||||||
@@ -2936,14 +2936,15 @@ test('loadGridViewMode reads from localStorage when scope is local', () => {
|
|||||||
_localStorageStore = {};
|
_localStorageStore = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('loadGridViewMode reads from serverSettings when scope is server', () => {
|
test('loadGridViewMode reads from localStorage regardless of viewPreferenceScope', () => {
|
||||||
// Set display settings with viewPreferenceScope 'server', server has grid_view_mode 'filtered'
|
// viewPreferenceScope is no longer used — loadGridViewMode always reads from localStorage
|
||||||
_localStorageStore = {};
|
_localStorageStore = {};
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ viewPreferenceScope: 'server' });
|
_localStorageStore['muxplex.display'] = JSON.stringify({ viewPreferenceScope: 'server', gridViewMode: 'filtered' });
|
||||||
app._setServerSettings({ grid_view_mode: 'filtered' });
|
app._setServerSettings({ grid_view_mode: 'grouped' });
|
||||||
|
|
||||||
const mode = app.loadGridViewMode();
|
const mode = app.loadGridViewMode();
|
||||||
assert.strictEqual(mode, 'filtered', 'loadGridViewMode should return grid_view_mode from serverSettings when scope is server');
|
// Must return localStorage value ('filtered'), NOT server value ('grouped')
|
||||||
|
assert.strictEqual(mode, 'filtered', 'loadGridViewMode should always return gridViewMode from localStorage');
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
_localStorageStore = {};
|
_localStorageStore = {};
|
||||||
@@ -3711,3 +3712,108 @@ test('applyFitLayout does NOT measure DOM dimensions (pure arithmetic)', () => {
|
|||||||
'applyFitLayout must NOT set inline tile heights — CSS grid 1fr rows handle sizing');
|
'applyFitLayout must NOT set inline tile heights — CSS grid 1fr rows handle sizing');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Settings tab reorganization (4 tabs) ────────────────────────────────────
|
||||||
|
|
||||||
|
test('HTML settings dialog has exactly 4 tab buttons', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||||
|
const tabMatches = source.match(/class="settings-tab[^"]*"\s+data-tab=/g) || [];
|
||||||
|
assert.strictEqual(tabMatches.length, 4, 'settings dialog must have exactly 4 tab buttons (not 5)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HTML index.html has no Notifications tab button', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||||
|
assert.ok(!source.includes('data-tab="notifications"'), 'Notifications tab button must be removed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HTML Sessions panel contains bell-sound checkbox', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||||
|
// Find the sessions PANEL div specifically (not the tab button)
|
||||||
|
const sessionsPanelStart = source.indexOf('<div class="settings-panel hidden" data-tab="sessions"');
|
||||||
|
assert.ok(sessionsPanelStart !== -1, 'sessions panel div must exist');
|
||||||
|
// Find the end: next settings-panel div
|
||||||
|
const nextPanel = source.indexOf('<div class="settings-panel', sessionsPanelStart + 1);
|
||||||
|
const sessionsPanelContent = source.substring(sessionsPanelStart, nextPanel !== -1 ? nextPanel : sessionsPanelStart + 4000);
|
||||||
|
assert.ok(sessionsPanelContent.includes('setting-bell-sound'), 'bell-sound checkbox must be in sessions panel');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HTML Sessions panel contains desktop notifications request button', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||||
|
// Find the sessions PANEL div specifically (not the tab button)
|
||||||
|
const sessionsPanelStart = source.indexOf('<div class="settings-panel hidden" data-tab="sessions"');
|
||||||
|
assert.ok(sessionsPanelStart !== -1, 'sessions panel div must exist');
|
||||||
|
const nextPanel = source.indexOf('<div class="settings-panel', sessionsPanelStart + 1);
|
||||||
|
const sessionsPanelContent = source.substring(sessionsPanelStart, nextPanel !== -1 ? nextPanel : sessionsPanelStart + 4000);
|
||||||
|
assert.ok(sessionsPanelContent.includes('notification-request-btn'), 'notification-request-btn must be in sessions panel');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HTML Display panel contains device name input outside #multi-device-fields', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||||
|
// Find the display PANEL div specifically (not the tab button)
|
||||||
|
const displayPanelStart = source.indexOf('<div class="settings-panel" data-tab="display"');
|
||||||
|
assert.ok(displayPanelStart !== -1, 'display panel div must exist');
|
||||||
|
const nextPanel = source.indexOf('<div class="settings-panel', displayPanelStart + 1);
|
||||||
|
const displayPanelContent = source.substring(displayPanelStart, nextPanel !== -1 ? nextPanel : displayPanelStart + 3000);
|
||||||
|
assert.ok(displayPanelContent.includes('setting-device-name'), 'device name input must be in display panel');
|
||||||
|
// Must NOT be inside #multi-device-fields
|
||||||
|
const multiDeviceIdx = displayPanelContent.indexOf('multi-device-fields');
|
||||||
|
const deviceNameIdx = displayPanelContent.indexOf('setting-device-name');
|
||||||
|
if (multiDeviceIdx !== -1) {
|
||||||
|
assert.ok(deviceNameIdx < multiDeviceIdx, 'device name input must NOT be inside #multi-device-fields');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('HTML index.html has no setting-view-scope select element', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
|
||||||
|
assert.ok(!source.includes('setting-view-scope'), '#setting-view-scope must be removed from HTML');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadGridViewMode always reads from localStorage (ignores viewPreferenceScope=server)', () => {
|
||||||
|
// After removing view scope, loadGridViewMode should ALWAYS use localStorage regardless
|
||||||
|
_localStorageStore = {};
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ viewPreferenceScope: 'server', gridViewMode: 'grouped' });
|
||||||
|
app._setServerSettings({ grid_view_mode: 'filtered' });
|
||||||
|
|
||||||
|
const mode = app.loadGridViewMode();
|
||||||
|
// Must return the localStorage value ('grouped'), NOT the server value ('filtered')
|
||||||
|
assert.strictEqual(mode, 'grouped', 'loadGridViewMode must always read from localStorage — server scope removed');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
_localStorageStore = {};
|
||||||
|
app._setServerSettings(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saveGridViewMode always writes to localStorage (ignores viewPreferenceScope=server)', () => {
|
||||||
|
_localStorageStore = {};
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ viewPreferenceScope: 'server' });
|
||||||
|
app._setServerSettings({ grid_view_mode: 'flat' });
|
||||||
|
|
||||||
|
const fetchCalls = [];
|
||||||
|
const origFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true, json: async () => ({}) }; };
|
||||||
|
|
||||||
|
app.saveGridViewMode('filtered');
|
||||||
|
|
||||||
|
// Must NOT call fetch (no server PATCH), must write to localStorage
|
||||||
|
assert.strictEqual(fetchCalls.length, 0, 'saveGridViewMode must NOT call fetch — server scope removed');
|
||||||
|
const saved = JSON.parse(_localStorageStore['muxplex.display'] || '{}');
|
||||||
|
assert.strictEqual(saved.gridViewMode, 'filtered', 'gridViewMode must be saved to localStorage');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
globalThis.fetch = origFetch;
|
||||||
|
_localStorageStore = {};
|
||||||
|
app._setGridViewMode('flat');
|
||||||
|
app._setServerSettings(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bindStaticEventListeners does NOT bind change event to setting-view-scope', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
|
||||||
|
const fnStart = source.indexOf('function bindStaticEventListeners(');
|
||||||
|
assert.ok(fnStart !== -1, 'bindStaticEventListeners must exist');
|
||||||
|
const fnEnd = source.indexOf('\nfunction ', fnStart + 1);
|
||||||
|
const fnBody = source.substring(fnStart, fnEnd > fnStart ? fnEnd : fnStart + 8000);
|
||||||
|
assert.ok(
|
||||||
|
!fnBody.includes('setting-view-scope'),
|
||||||
|
'bindStaticEventListeners must NOT reference setting-view-scope — view scope removed',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user