diff --git a/src/widgets/forms.js b/src/widgets/forms.js index 5aef219e3..682efb2a5 100644 --- a/src/widgets/forms.js +++ b/src/widgets/forms.js @@ -1903,7 +1903,14 @@ export function buildCheckboxForm (dom, kb, lab, del, ins, form, dataDoc, trista refresh() if (!editable) return box + let isUpdating = false // Prevent concurrent updates on double-click + const boxHandler = function (_e) { + if (isUpdating) { + return // Ignore clicks while update is in progress + } + isUpdating = true + input.disabled = true // Disable button to provide user feedback colorCarrier.style.color = '#bbb' // grey -- not saved yet const toDelete = input.state === true ? ins : input.state === false ? del : [] input.newState = @@ -1924,6 +1931,8 @@ export function buildCheckboxForm (dom, kb, lab, del, ins, form, dataDoc, trista success, errorBody ) { + isUpdating = false + input.disabled = false if (!success) { if (toDelete.why) { const hmmm = kb.holds( diff --git a/test/unit/widgets/forms/index.test.ts b/test/unit/widgets/forms/index.test.ts index c98d77128..93bc1f665 100644 --- a/test/unit/widgets/forms/index.test.ts +++ b/test/unit/widgets/forms/index.test.ts @@ -1,5 +1,5 @@ import { silenceDebugMessages } from '../../helpers/debugger' -import { namedNode } from 'rdflib' +import { namedNode, st } from 'rdflib' import ns from '../../../../src/ns' import { store } from 'solid-logic' @@ -594,6 +594,57 @@ describe('buildCheckboxForm', () => { ) ).toBeInstanceOf(HTMLDivElement) }) + + it('ignores rapid second click while async update is in progress and reenables button afterward', async () => { + const dataDoc = namedNode('http://example.com/#doc') + const form = namedNode('http://example.com/#form') + const subject = namedNode('http://example.com/#subject') + const predicate = namedNode('http://example.com/#predicate') + const object = namedNode('http://example.com/#object') + const statement = st(subject, predicate, object, dataDoc) + + const originalEditable = store.updater.editable + const originalUpdate = store.updater.update + + const updateSpy = jest.fn((_deletes, _inserts, callback) => { + return new Promise(resolve => { + setTimeout(() => { + callback('uri', true, 'ok') + resolve(true) + }, 0) + }) + }) + + store.updater.editable = jest.fn(() => true) as any + store.updater.update = updateSpy as any + + try { + const box = buildCheckboxForm( + document, + store, + 'label', + [], + statement, + form, + dataDoc, + false + ) + const checkboxButton = box.querySelector('button') as HTMLButtonElement + + checkboxButton.click() + checkboxButton.click() + + expect(updateSpy).toHaveBeenCalledTimes(1) + expect(checkboxButton.disabled).toEqual(true) + + await new Promise(resolve => setTimeout(resolve, 5)) + + expect(checkboxButton.disabled).toEqual(false) + } finally { + store.updater.editable = originalEditable + store.updater.update = originalUpdate + } + }) }) describe('newThing', () => {