From 17a464b7eef19b6cd16ef0ef84891044e3942c59 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 12 Jul 2026 11:36:33 -0700 Subject: [PATCH] fix(ui): lock workboard draft during save (#105600) --- ui/src/pages/workboard/view.test.ts | 6 +- ui/src/pages/workboard/view.ts | 72 ++++++++++++++++---- ui/src/pages/workboard/workboard.e2e.test.ts | 20 ++++++ ui/src/styles/workboard.css | 5 ++ 4 files changed, 87 insertions(+), 16 deletions(-) diff --git a/ui/src/pages/workboard/view.test.ts b/ui/src/pages/workboard/view.test.ts index da6c691b102e..99065b60e0fd 100644 --- a/ui/src/pages/workboard/view.test.ts +++ b/ui/src/pages/workboard/view.test.ts @@ -502,9 +502,9 @@ describe("renderWorkboard", () => { "Unsaved edit", ); - container - .querySelector('button[aria-label="Cancel"]') - ?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + const cancelButton = container.querySelector('button[aria-label="Cancel"]'); + expect(cancelButton?.disabled).toBe(false); + cancelButton?.click(); expect(state.draftOpen).toBe(false); }); diff --git a/ui/src/pages/workboard/view.ts b/ui/src/pages/workboard/view.ts index ddc5488decd6..e44035780659 100644 --- a/ui/src/pages/workboard/view.ts +++ b/ui/src/pages/workboard/view.ts @@ -342,13 +342,14 @@ function trapWorkboardDialogFocus(event: KeyboardEvent, root: HTMLElement) { function handleWorkboardDialogKeydown( event: KeyboardEvent, props: WorkboardProps, - close: () => void, + close: () => boolean | void, ) { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); - close(); - props.onRequestUpdate?.(); + if (close() !== false) { + props.onRequestUpdate?.(); + } return; } if (event.key === "Tab") { @@ -997,19 +998,33 @@ function renderWorkboardSelect(params: { requestUpdate?: () => void; className?: string; showLabel?: boolean; + disabled?: boolean; }) { const selected = params.options.find((option) => option.value === params.value); const selectedLabel = selected?.label ?? params.value; const select = html`
{ const details = event.currentTarget as HTMLDetailsElement; + if (params.disabled) { + details.open = false; + return; + } closeOtherWorkboardSelectMenus(details); positionWorkboardSelectMenu(details); syncWorkboardSelectDocumentCloser(); }} - @keydown=${handleWorkboardSelectKeydown} + @keydown=${(event: KeyboardEvent) => { + if (params.disabled) { + event.preventDefault(); + event.stopPropagation(); + return; + } + handleWorkboardSelectKeydown(event); + }} @focusout=${(event: FocusEvent) => { const details = event.currentTarget as HTMLDetailsElement; if (!(event.relatedTarget instanceof Node) || !details.contains(event.relatedTarget)) { @@ -1021,6 +1036,14 @@ function renderWorkboardSelect(params: { class="input workboard-select__trigger" aria-label=${`${params.label}: ${selectedLabel}`} aria-haspopup="listbox" + aria-disabled=${params.disabled ? "true" : "false"} + tabindex=${params.disabled ? "-1" : "0"} + @click=${(event: MouseEvent) => { + if (params.disabled) { + event.preventDefault(); + event.stopPropagation(); + } + }} > ${selectedLabel} @@ -1036,7 +1059,7 @@ function renderWorkboardSelect(params: { tabindex="-1" aria-selected=${optionSelected} aria-disabled=${option.disabled === true} - ?disabled=${option.disabled} + ?disabled=${params.disabled || option.disabled} @click=${(event: Event) => { if (option.disabled) { return; @@ -1479,13 +1502,22 @@ function renderCardModal(props: WorkboardProps) { const draftCommentBusy = editing && state.busyCardIds.has(state.editingCardId ?? ""); const draftActionsBusy = !canMutate(props) || state.loading || state.dispatching || draftCommentBusy; + // Save completion resets this shared draft. Lock every edit and dismissal path + // only for that write so stale drafts can still use Cancel to recover readiness. + const draftDismissalBusy = state.draftSaving; + const dismissDraft = () => { + if (draftDismissalBusy) { + return false; + } + resetDraft(state); + return true; + }; return html`