Compare commits

...

3 commits

Author SHA1 Message Date
838c31293e
Merge branch 'preserve-tree-expansion' 2026-05-01 11:48:49 +03:00
1342a67cf3
preserve expanded state across node re-render
introduce a module-level expandedNodeIds set that tracks which
nodes the user has manually expanded. renderTree consults the set
when deciding initial visibility (falling back to the depth-based
default for a fresh load), the toggle click handler keeps the set
in sync, and both add-child save handlers add the parent's id
before triggering the re-fetch. on a fresh load the set starts
empty so root-only-open behavior is unchanged and the existing
toggle tests keep passing.
2026-05-01 11:45:55 +03:00
dce4e4a4f6
test child add keeps parent expanded
assert that a non-root parent that the user expanded stays expanded
after adding a child. currently fails: fetchAndRenderNodes wipes
and rebuilds the tree with depth-based default visibility, so any
manually expanded non-root collapses on every save.
2026-05-01 11:44:24 +03:00
2 changed files with 50 additions and 1 deletions

View file

@ -65,6 +65,42 @@ describe('The admin text detail page', () => {
cy.get('#text-detail li').should('contain', 'Nested Child Node')
})
it('keeps a parent expanded after adding a child to it', () => {
cy.intercept('POST', '/api/nodes').as('createNode')
cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh')
cy.get('#text-detail > ul > li > ul > li')
.first()
.find('button.toggle-children')
.click()
cy.get('#text-detail > ul > li > ul > li > ul')
.first()
.should('be.visible')
cy.get('#text-detail > ul > li > ul > li')
.first()
.children('button.add-child')
.click()
cy.get('#text-detail > ul > li > ul > li')
.first()
.children('input.child-title')
.type('Shemos')
cy.get('#text-detail > ul > li > ul > li')
.first()
.children('button.save-child')
.click()
cy.wait('@createNode').its('response.statusCode').should('eq', 201)
cy.wait('@getNodesRefresh')
cy.get('#text-detail > ul > li > ul > li > ul')
.first()
.should('be.visible')
cy.get('#text-detail > ul > li > ul > li > ul')
.first()
.should('contain', 'Shemos')
})
it('newly added child persists after page reload', () => {
cy.intercept('POST', '/api/nodes').as('createNode')
cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh')

View file

@ -1,3 +1,5 @@
const expandedNodeIds = new Set();
document.addEventListener('DOMContentLoaded', () => {
const textId = window.location.pathname.split('/').pop();
@ -69,7 +71,11 @@ function renderTree(nodes, textId, depth = 0) {
if (node.children.length > 0) {
const childUl = renderTree(node.children, textId, depth + 1);
const childrenVisible = depth === 0;
const childrenVisible =
expandedNodeIds.has(node.id) || depth === 0;
if (childrenVisible) {
expandedNodeIds.add(node.id);
}
childUl.style.display = childrenVisible ? '' : 'none';
const toggleBtn = document.createElement('button');
@ -79,6 +85,11 @@ function renderTree(nodes, textId, depth = 0) {
const isHidden = childUl.style.display === 'none';
childUl.style.display = isHidden ? '' : 'none';
toggleBtn.textContent = isHidden ? '▼' : '▶';
if (isHidden) {
expandedNodeIds.add(node.id);
} else {
expandedNodeIds.delete(node.id);
}
});
li.appendChild(toggleBtn);
@ -110,6 +121,7 @@ function toggleAddForm(li, parentNodeId, textId) {
const title = input.value.trim();
if (!title) return;
expandedNodeIds.add(parentNodeId);
fetch('/api/nodes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -155,6 +167,7 @@ function toggleBulkAddForm(li, parentNodeId, textId) {
const count = parseInt(countInput.value);
if (!titlePrefix || !count || count < 1) return;
expandedNodeIds.add(parentNodeId);
fetch('/api/nodes/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },