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:
+87
-2
@@ -898,6 +898,87 @@ function closeViewDropdown() {
|
|||||||
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
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,
|
* 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.
|
||||||
@@ -2439,8 +2520,11 @@ function bindStaticEventListeners() {
|
|||||||
}
|
}
|
||||||
var action = e.target.closest('[data-action]');
|
var action = e.target.closest('[data-action]');
|
||||||
if (action) {
|
if (action) {
|
||||||
// action handlers (new-view, manage-views) — placeholder for future tasks
|
if (action.dataset.action === 'new-view') {
|
||||||
closeViewDropdown();
|
showNewViewInput();
|
||||||
|
} else {
|
||||||
|
closeViewDropdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2870,6 +2954,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
renderViewDropdown,
|
renderViewDropdown,
|
||||||
toggleViewDropdown,
|
toggleViewDropdown,
|
||||||
closeViewDropdown,
|
closeViewDropdown,
|
||||||
|
showNewViewInput,
|
||||||
switchView,
|
switchView,
|
||||||
// Federation tiles
|
// Federation tiles
|
||||||
buildStatusTileHTML,
|
buildStatusTileHTML,
|
||||||
|
|||||||
@@ -3229,3 +3229,35 @@ def test_backtick_only_on_grid_not_fullscreen() -> None:
|
|||||||
assert "'grid'" in body, (
|
assert "'grid'" in body, (
|
||||||
"handleGlobalKeydown must check _viewMode === 'grid' before handling backtick/number shortcuts"
|
"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