Compare commits

..

7 commits

Author SHA1 Message Date
dfa0bc6c00
Merge branch 'add-form-ux' 2026-05-02 20:48:14 +03:00
a1bfe4f7c1
close other add forms when opening a new one
introduce closeAllAddForms which strips every add-child and
bulk-add input/button from the tree, and call it at the start of
toggleAddForm and toggleBulkAddForm (after the same-li toggle-off
short-circuit, so clicking the same trigger still closes its own
form). enforces a single open add form across the whole tree.
2026-05-01 11:58:12 +03:00
d61d68571d
test only one add form open at a time
assert that opening any add-child or bulk-add form closes any
other open add form across the tree. currently fails: each toggle
function only checks for an open form on its own li.
2026-05-01 11:56:44 +03:00
bd14bfd7a1
submit bulk add form on enter key
extract the save-bulk handler into a submit closure shared by the
save button click and a keydown listener on both the title and
count inputs. focus the title input as soon as the form opens.
2026-05-01 11:55:43 +03:00
ff8ec9a2ab
test enter submits bulk add form
assert that pressing enter from either the bulk-title or
bulk-count input submits the bulk add form. currently fails:
only the save-bulk button click triggers the post.
2026-05-01 11:54:53 +03:00
3928fef213
submit add child form on enter key
extract the save-child handler into a submit closure shared by
the save button click and a keydown listener on the input. also
focus the input as soon as the form opens so the user can type
and hit enter without touching the mouse.
2026-05-01 11:53:54 +03:00
74705379cb
test enter submits add child form
assert that pressing enter while typing in the add-child input
submits the form. currently fails: only the save-child button
click triggers the post.
2026-05-01 11:53:06 +03:00
3 changed files with 142 additions and 3 deletions

View file

@ -101,6 +101,79 @@ describe('The admin text detail page', () => {
.should('contain', 'Shemos')
})
it('pressing Enter in the add-child input submits', () => {
cy.intercept('POST', '/api/nodes').as('createNode')
cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh')
cy.get('#text-detail > ul > li')
.first()
.children('button.add-child')
.click()
cy.get('#text-detail > ul > li')
.first()
.children('input.child-title')
.type('Enter Child{enter}')
cy.wait('@createNode').its('response.statusCode').should('eq', 201)
cy.wait('@getNodesRefresh')
cy.get('#text-detail li').should('contain', 'Enter Child')
})
it('opening add-child on another node closes the first one', () => {
cy.get('#text-detail > ul > li')
.first()
.children('button.add-child')
.click()
cy.get('#text-detail > ul > li')
.first()
.children('input.child-title')
.should('be.visible')
cy.get('#text-detail > ul > li > ul > li')
.first()
.children('button.add-child')
.click()
cy.get('#text-detail > ul > li')
.first()
.children('input.child-title')
.should('not.exist')
cy.get('#text-detail > ul > li')
.first()
.children('button.save-child')
.should('not.exist')
cy.get('#text-detail > ul > li > ul > li')
.first()
.children('input.child-title')
.should('be.visible')
})
it('opening bulk-add closes an open add-child form', () => {
cy.get('#text-detail > ul > li')
.first()
.children('button.add-child')
.click()
cy.get('#text-detail > ul > li')
.first()
.children('input.child-title')
.should('be.visible')
cy.get('#text-detail > ul > li > ul > li')
.first()
.children('button.bulk-add-children')
.click()
cy.get('#text-detail > ul > li')
.first()
.children('input.child-title')
.should('not.exist')
cy.get('#text-detail > ul > li > ul > li')
.first()
.children('input.bulk-title')
.should('be.visible')
})
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

@ -65,6 +65,30 @@ describe('Bulk add children on the admin text detail page', () => {
cy.get('@bulkCreate.all').should('have.length', 0)
})
it('pressing Enter in the bulk-count input submits', () => {
cy.intercept('POST', '/api/nodes/bulk').as('bulkCreate')
cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh')
cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click()
cy.get('#text-detail > ul > li').first().children('input.bulk-title').type('Enter')
cy.get('#text-detail > ul > li').first().children('input.bulk-count').type('2{enter}')
cy.wait('@bulkCreate').its('response.statusCode').should('eq', 201)
cy.wait('@getNodesRefresh')
cy.get('#text-detail li').should('contain', 'Enter 1')
cy.get('#text-detail li').should('contain', 'Enter 2')
})
it('pressing Enter in the bulk-title input submits', () => {
cy.intercept('POST', '/api/nodes/bulk').as('bulkCreate')
cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh')
cy.get('#text-detail > ul > li').first().children('button.bulk-add-children').click()
cy.get('#text-detail > ul > li').first().children('input.bulk-count').type('2')
cy.get('#text-detail > ul > li').first().children('input.bulk-title').type('Title{enter}')
cy.wait('@bulkCreate').its('response.statusCode').should('eq', 201)
cy.wait('@getNodesRefresh')
cy.get('#text-detail li').should('contain', 'Title 1')
cy.get('#text-detail li').should('contain', 'Title 2')
})
it('bulk added nodes persist after page reload', () => {
cy.intercept('POST', '/api/nodes/bulk').as('bulkCreate')
cy.intercept('GET', '/api/nodes/0').as('getNodesRefresh')

View file

@ -101,6 +101,21 @@ function renderTree(nodes, textId, depth = 0) {
return ul;
}
function closeAllAddForms() {
const selectors = [
'input.child-title',
'button.save-child',
'input.bulk-title',
'input.bulk-count',
'button.save-bulk',
];
selectors.forEach((selector) => {
document
.querySelectorAll('#text-detail ' + selector)
.forEach((element) => element.remove());
});
}
function toggleAddForm(li, parentNodeId, textId) {
const existing = li.querySelector('input.child-title');
if (existing) {
@ -109,6 +124,8 @@ function toggleAddForm(li, parentNodeId, textId) {
return;
}
closeAllAddForms();
const input = document.createElement('input');
input.type = 'text';
input.className = 'child-title';
@ -117,7 +134,8 @@ function toggleAddForm(li, parentNodeId, textId) {
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save';
saveBtn.className = 'save-child';
saveBtn.addEventListener('click', () => {
function submit() {
const title = input.value.trim();
if (!title) return;
@ -133,10 +151,19 @@ function toggleAddForm(li, parentNodeId, textId) {
return res.json();
})
.then(() => fetchAndRenderNodes(textId));
}
saveBtn.addEventListener('click', submit);
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
submit();
}
});
li.appendChild(input);
li.appendChild(saveBtn);
input.focus();
}
function toggleBulkAddForm(li, parentNodeId, textId) {
@ -148,6 +175,8 @@ function toggleBulkAddForm(li, parentNodeId, textId) {
return;
}
closeAllAddForms();
const titleInput = document.createElement('input');
titleInput.type = 'text';
titleInput.className = 'bulk-title';
@ -162,7 +191,8 @@ function toggleBulkAddForm(li, parentNodeId, textId) {
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save';
saveBtn.className = 'save-bulk';
saveBtn.addEventListener('click', () => {
function submit() {
const titlePrefix = titleInput.value.trim();
const count = parseInt(countInput.value);
if (!titlePrefix || !count || count < 1) return;
@ -179,9 +209,21 @@ function toggleBulkAddForm(li, parentNodeId, textId) {
return res.json();
})
.then(() => fetchAndRenderNodes(textId));
});
}
function submitOnEnter(event) {
if (event.key === 'Enter') {
event.preventDefault();
submit();
}
}
saveBtn.addEventListener('click', submit);
titleInput.addEventListener('keydown', submitOnEnter);
countInput.addEventListener('keydown', submitOnEnter);
li.appendChild(titleInput);
li.appendChild(countInput);
li.appendChild(saveBtn);
titleInput.focus();
}