From 16a6fa946a9f245fa3347d3ff7107ae8dd79eb61 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 19 Mar 2026 17:51:55 +1100 Subject: [PATCH 1/2] set updating on click --- src/widgets/forms.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/widgets/forms.js b/src/widgets/forms.js index 4c69ecd09..7cfdc84a6 100644 --- a/src/widgets/forms.js +++ b/src/widgets/forms.js @@ -1879,7 +1879,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 = @@ -1900,6 +1907,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( From 70f846efe3ec0aeae20d21878ce4280999683f0b Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 06:50:19 +1100 Subject: [PATCH 2/2] add test for double click --- test/unit/widgets/forms/index.test.ts | 53 ++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) 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', () => {