feat: add Views settings tab with inline rename, reorder, and delete
Implements the "Manage Views" Settings Tab with full UI and functionality: HTML: Added Views tab button and panel with empty state message. JS: Added renderViewsSettingsTab() function that renders the view list with: - Clickable name for inline rename (validates reserved/duplicate names) - Session count display - Up/down arrow buttons for reordering (disabled at boundaries) - Delete button with inline 'Sure? [Yes] [No]' confirmation - Delegated event handlers for all interactions Added _saveViewsAndRerender() helper that PATCHes /api/settings, updates _serverSettings, and re-renders both tab and dropdown. Updated openSettings() to call renderViewsSettingsTab() after loading. CSS: Added styling for views-settings-list, views-settings-row, views-settings-name, views-settings-count, views-settings-actions, views-settings-btn, views-settings-confirm, and rename input. Tests: Added 3 new frontend tests and updated existing panel count test. All 491 frontend tests pass with no regressions. Generated with Amplifier Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
+251
-1
@@ -892,7 +892,7 @@ function closeViewDropdown() {
|
|||||||
if (menu) {
|
if (menu) {
|
||||||
menu.classList.add('hidden');
|
menu.classList.add('hidden');
|
||||||
// Remove any inline new-view input
|
// Remove any inline new-view input
|
||||||
var newViewInput = menu.querySelector('.view-dropdown-new-input');
|
var newViewInput = menu.querySelector('.view-dropdown__new-input');
|
||||||
if (newViewInput) newViewInput.remove();
|
if (newViewInput) newViewInput.remove();
|
||||||
}
|
}
|
||||||
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
||||||
@@ -979,6 +979,250 @@ function showNewViewInput() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save updated views array via PATCH /api/settings, update _serverSettings,
|
||||||
|
* re-render the views settings tab, and re-render the view dropdown.
|
||||||
|
* @param {Array} updatedViews - New views array to save.
|
||||||
|
*/
|
||||||
|
function _saveViewsAndRerender(updatedViews) {
|
||||||
|
return api('PATCH', '/api/settings', { views: updatedViews })
|
||||||
|
.then(function() {
|
||||||
|
if (_serverSettings) _serverSettings.views = updatedViews;
|
||||||
|
renderViewsSettingsTab();
|
||||||
|
renderViewDropdown();
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
showToast('Failed to save views');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the Views settings tab content.
|
||||||
|
* Reads views from _serverSettings and builds an interactive list
|
||||||
|
* with inline rename, up/down reorder, and delete with confirmation.
|
||||||
|
*/
|
||||||
|
function renderViewsSettingsTab() {
|
||||||
|
var listEl = $('views-settings-list');
|
||||||
|
var emptyEl = $('views-settings-empty');
|
||||||
|
if (!listEl) return;
|
||||||
|
|
||||||
|
var views = (_serverSettings && _serverSettings.views) || [];
|
||||||
|
|
||||||
|
if (views.length === 0) {
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
if (emptyEl) emptyEl.style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emptyEl) emptyEl.style.display = 'none';
|
||||||
|
|
||||||
|
// Build the list of view rows
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
views.forEach(function(view, idx) {
|
||||||
|
var sessions = (_serverSettings && _serverSettings.sessions) || [];
|
||||||
|
// Count sessions that belong to this view
|
||||||
|
var viewSessions = view.sessions || [];
|
||||||
|
var sessionCount = viewSessions.length;
|
||||||
|
|
||||||
|
var row = document.createElement('div');
|
||||||
|
row.className = 'views-settings-row';
|
||||||
|
row.setAttribute('data-view-idx', String(idx));
|
||||||
|
|
||||||
|
// Name span (clickable for rename)
|
||||||
|
var nameSpan = document.createElement('span');
|
||||||
|
nameSpan.className = 'views-settings-name';
|
||||||
|
nameSpan.textContent = view.name;
|
||||||
|
nameSpan.title = 'Click to rename';
|
||||||
|
|
||||||
|
// Session count
|
||||||
|
var countSpan = document.createElement('span');
|
||||||
|
countSpan.className = 'views-settings-count';
|
||||||
|
countSpan.textContent = sessionCount + (sessionCount === 1 ? ' session' : ' sessions');
|
||||||
|
|
||||||
|
// Actions container
|
||||||
|
var actionsDiv = document.createElement('div');
|
||||||
|
actionsDiv.className = 'views-settings-actions';
|
||||||
|
|
||||||
|
// Up button
|
||||||
|
var upBtn = document.createElement('button');
|
||||||
|
upBtn.className = 'views-settings-btn';
|
||||||
|
upBtn.textContent = '▲';
|
||||||
|
upBtn.title = 'Move up';
|
||||||
|
upBtn.setAttribute('data-action', 'move-up');
|
||||||
|
upBtn.setAttribute('data-idx', String(idx));
|
||||||
|
if (idx === 0) upBtn.disabled = true;
|
||||||
|
|
||||||
|
// Down button
|
||||||
|
var downBtn = document.createElement('button');
|
||||||
|
downBtn.className = 'views-settings-btn';
|
||||||
|
downBtn.textContent = '▼';
|
||||||
|
downBtn.title = 'Move down';
|
||||||
|
downBtn.setAttribute('data-action', 'move-down');
|
||||||
|
downBtn.setAttribute('data-idx', String(idx));
|
||||||
|
if (idx === views.length - 1) downBtn.disabled = true;
|
||||||
|
|
||||||
|
// Delete button
|
||||||
|
var deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'views-settings-btn views-settings-btn--danger';
|
||||||
|
deleteBtn.textContent = 'Delete';
|
||||||
|
deleteBtn.setAttribute('data-action', 'delete');
|
||||||
|
deleteBtn.setAttribute('data-idx', String(idx));
|
||||||
|
|
||||||
|
actionsDiv.appendChild(upBtn);
|
||||||
|
actionsDiv.appendChild(downBtn);
|
||||||
|
actionsDiv.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
row.appendChild(nameSpan);
|
||||||
|
row.appendChild(countSpan);
|
||||||
|
row.appendChild(actionsDiv);
|
||||||
|
listEl.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delegated click handler on the list
|
||||||
|
listEl.onclick = function(e) {
|
||||||
|
var views = (_serverSettings && _serverSettings.views) || [];
|
||||||
|
var target = e.target;
|
||||||
|
|
||||||
|
// Move up
|
||||||
|
if (target.getAttribute('data-action') === 'move-up') {
|
||||||
|
var idx = parseInt(target.getAttribute('data-idx'), 10);
|
||||||
|
if (idx > 0) {
|
||||||
|
var updated = views.slice();
|
||||||
|
var tmp = updated[idx - 1];
|
||||||
|
updated[idx - 1] = updated[idx];
|
||||||
|
updated[idx] = tmp;
|
||||||
|
_saveViewsAndRerender(updated);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move down
|
||||||
|
if (target.getAttribute('data-action') === 'move-down') {
|
||||||
|
var idx = parseInt(target.getAttribute('data-idx'), 10);
|
||||||
|
if (idx < views.length - 1) {
|
||||||
|
var updated = views.slice();
|
||||||
|
var tmp = updated[idx + 1];
|
||||||
|
updated[idx + 1] = updated[idx];
|
||||||
|
updated[idx] = tmp;
|
||||||
|
_saveViewsAndRerender(updated);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete: show inline confirm
|
||||||
|
if (target.getAttribute('data-action') === 'delete') {
|
||||||
|
var idx = parseInt(target.getAttribute('data-idx'), 10);
|
||||||
|
var row = listEl.querySelector('[data-view-idx="' + idx + '"]');
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
// Replace delete button with "Sure? [Yes] [No]"
|
||||||
|
var actionsDiv = row.querySelector('.views-settings-actions');
|
||||||
|
if (!actionsDiv) return;
|
||||||
|
actionsDiv.innerHTML = '';
|
||||||
|
|
||||||
|
var confirmSpan = document.createElement('span');
|
||||||
|
confirmSpan.className = 'views-settings-confirm';
|
||||||
|
confirmSpan.textContent = 'Sure? ';
|
||||||
|
|
||||||
|
var yesBtn = document.createElement('button');
|
||||||
|
yesBtn.className = 'views-settings-btn views-settings-btn--danger';
|
||||||
|
yesBtn.textContent = 'Yes';
|
||||||
|
yesBtn.setAttribute('data-action', 'confirm-delete');
|
||||||
|
yesBtn.setAttribute('data-idx', String(idx));
|
||||||
|
|
||||||
|
var noBtn = document.createElement('button');
|
||||||
|
noBtn.className = 'views-settings-btn';
|
||||||
|
noBtn.textContent = 'No';
|
||||||
|
noBtn.setAttribute('data-action', 'cancel-delete');
|
||||||
|
|
||||||
|
confirmSpan.appendChild(yesBtn);
|
||||||
|
confirmSpan.appendChild(document.createTextNode(' '));
|
||||||
|
confirmSpan.appendChild(noBtn);
|
||||||
|
actionsDiv.appendChild(confirmSpan);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm delete
|
||||||
|
if (target.getAttribute('data-action') === 'confirm-delete') {
|
||||||
|
var idx = parseInt(target.getAttribute('data-idx'), 10);
|
||||||
|
var updated = views.slice();
|
||||||
|
updated.splice(idx, 1);
|
||||||
|
// If deleting the active view, fall back to 'all'
|
||||||
|
if (_activeView === views[idx].name) {
|
||||||
|
_activeView = 'all';
|
||||||
|
}
|
||||||
|
_saveViewsAndRerender(updated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel delete: re-render
|
||||||
|
if (target.getAttribute('data-action') === 'cancel-delete') {
|
||||||
|
renderViewsSettingsTab();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click on name: inline rename
|
||||||
|
if (target.classList.contains('views-settings-name')) {
|
||||||
|
var row = target.closest('.views-settings-row');
|
||||||
|
if (!row) return;
|
||||||
|
var idx = parseInt(row.getAttribute('data-view-idx'), 10);
|
||||||
|
var currentName = views[idx] && views[idx].name;
|
||||||
|
if (currentName === undefined) return;
|
||||||
|
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.className = 'views-settings-rename-input';
|
||||||
|
input.value = currentName;
|
||||||
|
target.parentNode.replaceChild(input, target);
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
|
||||||
|
function commitRename() {
|
||||||
|
var newName = input.value.trim();
|
||||||
|
if (!newName || newName === currentName) {
|
||||||
|
renderViewsSettingsTab();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Validate: not reserved
|
||||||
|
if (newName === 'all' || newName === 'hidden') {
|
||||||
|
showToast('Cannot use reserved name \'' + newName + '\'');
|
||||||
|
renderViewsSettingsTab();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Validate: not duplicate
|
||||||
|
var duplicate = views.find(function(v, i) { return i !== idx && v.name === newName; });
|
||||||
|
if (duplicate) {
|
||||||
|
showToast('View \'' + newName + '\' already exists');
|
||||||
|
renderViewsSettingsTab();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Update the view name
|
||||||
|
var updated = views.map(function(v, i) {
|
||||||
|
return i === idx ? Object.assign({}, v, { name: newName }) : v;
|
||||||
|
});
|
||||||
|
// If this was the active view, update _activeView
|
||||||
|
if (_activeView === currentName) {
|
||||||
|
_activeView = newName;
|
||||||
|
}
|
||||||
|
_saveViewsAndRerender(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
commitRename();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
renderViewsSettingsTab();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
commitRename();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch to a named view. Updates _activeView, re-renders the grid and sidebar,
|
* Switch to a named view. Updates _activeView, re-renders the grid and sidebar,
|
||||||
* updates the dropdown label, and persists the change via PATCH /api/state.
|
* updates the dropdown label, and persists the change via PATCH /api/state.
|
||||||
@@ -1990,6 +2234,9 @@ function openSettings() {
|
|||||||
if (deleteTemplateEl) {
|
if (deleteTemplateEl) {
|
||||||
deleteTemplateEl.value = (ss && ss.delete_session_template) || DELETE_SESSION_DEFAULT_TEMPLATE;
|
deleteTemplateEl.value = (ss && ss.delete_session_template) || DELETE_SESSION_DEFAULT_TEMPLATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Views tab
|
||||||
|
renderViewsSettingsTab();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2956,6 +3203,9 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
closeViewDropdown,
|
closeViewDropdown,
|
||||||
showNewViewInput,
|
showNewViewInput,
|
||||||
switchView,
|
switchView,
|
||||||
|
// Manage Views settings tab
|
||||||
|
renderViewsSettingsTab,
|
||||||
|
_saveViewsAndRerender,
|
||||||
// Federation tiles
|
// Federation tiles
|
||||||
buildStatusTileHTML,
|
buildStatusTileHTML,
|
||||||
// Constants
|
// Constants
|
||||||
|
|||||||
@@ -103,6 +103,7 @@
|
|||||||
<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="views">Views</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>
|
||||||
@@ -199,6 +200,10 @@
|
|||||||
<div id="setting-hidden-sessions" class="settings-checkbox-list"></div>
|
<div id="setting-hidden-sessions" class="settings-checkbox-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-panel hidden" data-tab="views">
|
||||||
|
<div id="views-settings-list" class="views-settings-list"></div>
|
||||||
|
<p class="settings-helper" id="views-settings-empty" style="display:none">No user-created views yet. Use the dropdown in the header to create one.</p>
|
||||||
|
</div>
|
||||||
<div class="settings-panel hidden" data-tab="new-session">
|
<div class="settings-panel hidden" data-tab="new-session">
|
||||||
<div class="settings-field settings-field--column">
|
<div class="settings-field settings-field--column">
|
||||||
<label class="settings-label" for="setting-template">Command template</label>
|
<label class="settings-label" for="setting-template">Command template</label>
|
||||||
|
|||||||
@@ -2001,3 +2001,99 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Manage Views settings tab (task-9) ─────────────────────────────────── */
|
||||||
|
|
||||||
|
.views-settings-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-name:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 3px 7px;
|
||||||
|
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-btn:hover:not(:disabled) {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-btn:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-btn--danger:hover:not(:disabled) {
|
||||||
|
border-color: #ef4444;
|
||||||
|
color: #ef4444;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-confirm {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-settings-rename-input {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -545,8 +545,8 @@ def test_html_settings_panels_use_data_tab() -> None:
|
|||||||
dialog = soup.find(id="settings-dialog")
|
dialog = soup.find(id="settings-dialog")
|
||||||
assert dialog is not None, "Missing #settings-dialog"
|
assert dialog is not None, "Missing #settings-dialog"
|
||||||
panels = dialog.find_all(class_="settings-panel")
|
panels = dialog.find_all(class_="settings-panel")
|
||||||
assert len(panels) == 4, (
|
assert len(panels) == 5, (
|
||||||
f"Expected 4 .settings-panel elements, found: {len(panels)}"
|
f"Expected 5 .settings-panel elements, found: {len(panels)}"
|
||||||
)
|
)
|
||||||
for panel in panels:
|
for panel in panels:
|
||||||
assert panel.get("data-tab") is not None, (
|
assert panel.get("data-tab") is not None, (
|
||||||
@@ -1464,3 +1464,40 @@ def test_view_dropdown_menu_has_role_menu() -> None:
|
|||||||
assert menu.get("role") == "menu", (
|
assert menu.get("role") == "menu", (
|
||||||
f"#view-dropdown-menu must have role='menu', got: {menu.get('role')!r}"
|
f"#view-dropdown-menu must have role='menu', got: {menu.get('role')!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Manage Views settings tab (task-9)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_has_views_tab_button() -> None:
|
||||||
|
"""Settings dialog must contain a tab button with data-tab='views'."""
|
||||||
|
soup = _SOUP
|
||||||
|
dialog = soup.find(id="settings-dialog")
|
||||||
|
assert dialog is not None, "Missing #settings-dialog"
|
||||||
|
tabs_container = dialog.find("nav", class_="settings-tabs")
|
||||||
|
assert tabs_container is not None, "Missing nav.settings-tabs"
|
||||||
|
tab = tabs_container.find("button", attrs={"data-tab": "views"})
|
||||||
|
assert tab is not None, "Missing tab button with data-tab='views' in settings-tabs"
|
||||||
|
tab_classes = tab.get("class") or []
|
||||||
|
assert "settings-tab" in tab_classes, (
|
||||||
|
f"views tab button must have class 'settings-tab', has: {tab_classes}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_has_views_panel() -> None:
|
||||||
|
"""A settings-panel with data-tab='views' must exist inside #settings-dialog."""
|
||||||
|
soup = _SOUP
|
||||||
|
dialog = soup.find(id="settings-dialog")
|
||||||
|
assert dialog is not None, "Missing #settings-dialog"
|
||||||
|
panel = dialog.find(class_="settings-panel", attrs={"data-tab": "views"})
|
||||||
|
assert panel is not None, (
|
||||||
|
"Missing settings-panel[data-tab='views'] (Views tab panel)"
|
||||||
|
)
|
||||||
|
# Must contain the views settings list
|
||||||
|
views_list = panel.find(id="views-settings-list")
|
||||||
|
assert views_list is not None, "Missing #views-settings-list inside views panel"
|
||||||
|
# Must contain the empty message element
|
||||||
|
empty_msg = panel.find(id="views-settings-empty")
|
||||||
|
assert empty_msg is not None, "Missing #views-settings-empty inside views panel"
|
||||||
|
|||||||
@@ -3261,3 +3261,15 @@ def test_show_new_view_input_patches_settings() -> None:
|
|||||||
assert "views" in body, (
|
assert "views" in body, (
|
||||||
"showNewViewInput must include 'views' in the PATCH body"
|
"showNewViewInput must include 'views' in the PATCH body"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Manage Views settings tab (task-9)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_views_settings_tab_function_exists() -> None:
|
||||||
|
"""renderViewsSettingsTab function must exist in app.js."""
|
||||||
|
assert "function renderViewsSettingsTab" in _JS, (
|
||||||
|
"renderViewsSettingsTab must be defined in app.js"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user