feat: add showNewViewInput inline creation flow for New View dropdown
- Add showNewViewInput() function after closeViewDropdown() in app.js
- Creates inline text input replacing '+ New View' button in dropdown
- Input: type=text, class=view-dropdown__new-input, placeholder='View name',
maxLength=30, aria-label='New view name'
- Re-focuses existing input if already present (prevents duplicates)
- On Enter: validates name (non-empty, non-reserved, non-duplicate),
PATCHes /api/settings with updated views, switches to new view on success
- On Escape: closes the dropdown
- On blur: closes dropdown after 150ms delay if input not focused
- Update bindStaticEventListeners to call showNewViewInput() for data-action='new-view'
instead of just closeViewDropdown()
- Export showNewViewInput in module.exports block
Tests added:
- test_show_new_view_input_function_exists: verifies function exists in app.js
- test_show_new_view_input_patches_settings: verifies PATCH /api/settings with views
Task: task-8
This commit is contained in:
+86
-1
@@ -898,6 +898,87 @@ function closeViewDropdown() {
|
||||
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an inline text input inside the view dropdown for creating a new view.
|
||||
* Replaces the '+ New View' button with a text input inside the dropdown menu.
|
||||
* - Removes any existing input and re-focuses it if already present.
|
||||
* - On Enter: validates name (not empty, not reserved, not duplicate),
|
||||
* then PATCHes /api/settings with the new view appended to views,
|
||||
* updates _serverSettings.views on success, and calls switchView(name).
|
||||
* - On Escape: closes the dropdown.
|
||||
* - On blur: closes the dropdown after 150ms if input is no longer focused.
|
||||
*/
|
||||
function showNewViewInput() {
|
||||
var menu = $('view-dropdown-menu');
|
||||
if (!menu) return;
|
||||
|
||||
// Re-focus existing input instead of creating a duplicate
|
||||
var existing = menu.querySelector('.view-dropdown__new-input');
|
||||
if (existing) {
|
||||
existing.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the '+ New View' button to replace
|
||||
var newViewBtn = menu.querySelector('[data-action="new-view"]');
|
||||
if (!newViewBtn) return;
|
||||
|
||||
// Create the inline text input
|
||||
var input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'view-dropdown__new-input';
|
||||
input.placeholder = 'View name';
|
||||
input.maxLength = 30;
|
||||
input.setAttribute('aria-label', 'New view name');
|
||||
|
||||
// Replace the '+ New View' button with the input
|
||||
newViewBtn.parentNode.replaceChild(input, newViewBtn);
|
||||
input.focus();
|
||||
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
var name = input.value.trim();
|
||||
|
||||
// Validate: not empty
|
||||
if (!name) return;
|
||||
|
||||
// Validate: not reserved
|
||||
if (name === 'all' || name === 'hidden') {
|
||||
showToast('Cannot use reserved name \'' + name + '\'');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate: not duplicate
|
||||
var views = (_serverSettings && _serverSettings.views) || [];
|
||||
if (views.find(function(v) { return v.name === name; })) {
|
||||
showToast('View \'' + name + '\' already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create view and PATCH /api/settings
|
||||
var updatedViews = views.concat([{ name: name, sessions: [] }]);
|
||||
api('PATCH', '/api/settings', { views: updatedViews })
|
||||
.then(function() {
|
||||
if (_serverSettings) _serverSettings.views = updatedViews;
|
||||
switchView(name);
|
||||
})
|
||||
.catch(function() {
|
||||
showToast('Failed to create view');
|
||||
});
|
||||
} else if (e.key === 'Escape') {
|
||||
closeViewDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
setTimeout(function() {
|
||||
if (document.activeElement !== input) {
|
||||
closeViewDropdown();
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -2439,9 +2520,12 @@ function bindStaticEventListeners() {
|
||||
}
|
||||
var action = e.target.closest('[data-action]');
|
||||
if (action) {
|
||||
// action handlers (new-view, manage-views) — placeholder for future tasks
|
||||
if (action.dataset.action === 'new-view') {
|
||||
showNewViewInput();
|
||||
} else {
|
||||
closeViewDropdown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2870,6 +2954,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
renderViewDropdown,
|
||||
toggleViewDropdown,
|
||||
closeViewDropdown,
|
||||
showNewViewInput,
|
||||
switchView,
|
||||
// Federation tiles
|
||||
buildStatusTileHTML,
|
||||
|
||||
@@ -3229,3 +3229,35 @@ def test_backtick_only_on_grid_not_fullscreen() -> None:
|
||||
assert "'grid'" in body, (
|
||||
"handleGlobalKeydown must check _viewMode === 'grid' before handling backtick/number shortcuts"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# New View inline creation flow (task-8)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_show_new_view_input_function_exists() -> None:
|
||||
"""app.js defines showNewViewInput for inline view creation."""
|
||||
assert "function showNewViewInput" in _JS, (
|
||||
"showNewViewInput must be defined in app.js"
|
||||
)
|
||||
|
||||
|
||||
def test_show_new_view_input_patches_settings() -> None:
|
||||
"""showNewViewInput handler PATCHes /api/settings with views on Enter."""
|
||||
match = re.search(
|
||||
r"function showNewViewInput\s*\(\s*\)\s*\{(.*?)(?=\nasync function |\nfunction |\n// )",
|
||||
_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert match, "showNewViewInput function not found"
|
||||
body = match.group(1)
|
||||
assert "PATCH" in body, (
|
||||
"showNewViewInput must PATCH /api/settings to create the view"
|
||||
)
|
||||
assert "/api/settings" in body, (
|
||||
"showNewViewInput must PATCH /api/settings with the updated views"
|
||||
)
|
||||
assert "views" in body, (
|
||||
"showNewViewInput must include 'views' in the PATCH body"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user