mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix(ui): stop Workboard draft fields reverting during edits (#115141)
* fix(ui): preserve Workboard draft edits * chore: leave release notes to release automation * test(ui): wait for settings takeover controls
This commit is contained in:
committed by
GitHub
parent
2cdd1246ed
commit
8944331f28
@@ -36,6 +36,15 @@ async function roundedWidth(locator: Locator): Promise<number> {
|
||||
return Math.round((await locator.boundingBox())?.width ?? 0);
|
||||
}
|
||||
|
||||
async function waitForSettingsSidebar(page: Page) {
|
||||
const sidebar = page.locator(".settings-sidebar");
|
||||
const search = sidebar.getByRole("searchbox", { name: "Search settings" });
|
||||
await sidebar.waitFor({ state: "visible" });
|
||||
// The route shell can paint before the takeover controls settle on a loaded CI host.
|
||||
await search.waitFor({ state: "visible" });
|
||||
return { search, sidebar };
|
||||
}
|
||||
|
||||
function visibleDrawerButton(page: Page) {
|
||||
return page.locator(".topbar-nav-toggle:visible, .chat-pane__nav-toggle:visible").first();
|
||||
}
|
||||
@@ -160,12 +169,11 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}settings/general`);
|
||||
const settingsSidebar = page.locator(".settings-sidebar");
|
||||
const { search: settingsSearchInput, sidebar: settingsSidebar } =
|
||||
await waitForSettingsSidebar(page);
|
||||
const settingsSearchShell = settingsSidebar.locator(".settings-sidebar__search");
|
||||
const settingsSearchInput = settingsSidebar.locator(".settings-sidebar__search-input");
|
||||
const settingsNav = settingsSidebar.locator(".settings-sidebar__nav");
|
||||
const firstSettingsLink = settingsSidebar.locator(".settings-sidebar__item").first();
|
||||
await settingsSidebar.waitFor();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page
|
||||
@@ -333,8 +341,8 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
await expect.poll(() => identityCard.isVisible()).toBe(true);
|
||||
await openSettingsFromIdentity();
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/general");
|
||||
const settingsSidebar = page.locator(".settings-sidebar");
|
||||
await expect.poll(() => settingsSidebar.isVisible()).toBe(true);
|
||||
const { search: settingsSearch, sidebar: settingsSidebar } =
|
||||
await waitForSettingsSidebar(page);
|
||||
await expect.poll(() => sidebar.isVisible()).toBe(false);
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -347,12 +355,8 @@ describeControlUiE2e("Control UI sidebar customization mocked Gateway E2E", () =
|
||||
await captureUiProof(page, "01a-settings-takeover.png");
|
||||
await captureSettingsSidebarProof(settingsSidebar, "01a-settings-search-initial.png");
|
||||
await holdUiProof(page);
|
||||
const settingsSearch = settingsSidebar.getByRole("searchbox", {
|
||||
name: "Search settings",
|
||||
});
|
||||
const settingsLinks = settingsSidebar.locator(".settings-sidebar__item");
|
||||
const allSettingsLabels = await trimmedTextContents(settingsLinks);
|
||||
await expect.poll(() => settingsSearch.isVisible()).toBe(true);
|
||||
await expect
|
||||
.poll(() =>
|
||||
settingsSearch.evaluate((input) => {
|
||||
|
||||
@@ -28,6 +28,36 @@ const workboardCardModalTitleId = "workboard-card-modal-title";
|
||||
const workboardCardModalDescriptionId = "workboard-card-modal-description";
|
||||
export const workboardCardModalId = "workboard-card-modal";
|
||||
|
||||
// Keep keystroke state local to the form. A parent render can restore stale
|
||||
// controlled values before the next field is edited or the draft is submitted.
|
||||
function syncDraftTextInput(
|
||||
state: WorkboardUiState,
|
||||
form: HTMLFormElement,
|
||||
input: HTMLInputElement | HTMLTextAreaElement,
|
||||
draftActionsBusy: boolean,
|
||||
) {
|
||||
if (input.classList.contains("workboard-draft__title")) {
|
||||
state.draftTitle = input.value;
|
||||
} else if (input.classList.contains("workboard-draft__notes")) {
|
||||
state.draftNotes = input.value;
|
||||
} else if (input.classList.contains("workboard-draft__labels")) {
|
||||
state.draftLabels = input.value;
|
||||
} else if (input.classList.contains("workboard-comments__input")) {
|
||||
state.draftCommentBody = input.value;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const draftSubmit = form.querySelector<HTMLButtonElement>(".workboard-draft__submit");
|
||||
if (draftSubmit) {
|
||||
draftSubmit.disabled = draftActionsBusy || !state.draftTitle.trim();
|
||||
}
|
||||
const commentSubmit = form.querySelector<HTMLButtonElement>(".workboard-comments__submit");
|
||||
if (commentSubmit) {
|
||||
commentSubmit.disabled = draftActionsBusy || !state.draftCommentBody.trim();
|
||||
}
|
||||
}
|
||||
|
||||
function defineTemplate(
|
||||
id: WorkboardTemplateId,
|
||||
draftKey: string,
|
||||
@@ -168,6 +198,17 @@ export function renderCardModal(props: WorkboardProps) {
|
||||
id=${workboardCardModalId}
|
||||
class="workboard-draft"
|
||||
aria-busy=${draftActionsBusy ? "true" : "false"}
|
||||
@input=${(event: InputEvent) => {
|
||||
const input = event.target;
|
||||
if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
|
||||
syncDraftTextInput(
|
||||
state,
|
||||
event.currentTarget as HTMLFormElement,
|
||||
input,
|
||||
draftActionsBusy,
|
||||
);
|
||||
}
|
||||
}}
|
||||
@submit=${(event: SubmitEvent) => {
|
||||
event.preventDefault();
|
||||
if (draftActionsBusy) {
|
||||
@@ -238,10 +279,6 @@ export function renderCardModal(props: WorkboardProps) {
|
||||
placeholder=${t("workboard.titlePlaceholder")}
|
||||
?disabled=${draftActionsBusy}
|
||||
.value=${state.draftTitle}
|
||||
@input=${(event: InputEvent) => {
|
||||
state.draftTitle = (event.currentTarget as HTMLInputElement).value;
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label class="workboard-field">
|
||||
@@ -251,10 +288,6 @@ export function renderCardModal(props: WorkboardProps) {
|
||||
placeholder=${t("workboard.notesPlaceholder")}
|
||||
?disabled=${draftActionsBusy}
|
||||
.value=${state.draftNotes}
|
||||
@input=${(event: InputEvent) => {
|
||||
state.draftNotes = (event.currentTarget as HTMLTextAreaElement).value;
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
></textarea>
|
||||
</label>
|
||||
</div>
|
||||
@@ -306,14 +339,10 @@ export function renderCardModal(props: WorkboardProps) {
|
||||
<label class="workboard-field workboard-field--wide">
|
||||
<span>${t("workboard.fieldLabels")}</span>
|
||||
<input
|
||||
class="input"
|
||||
class="input workboard-draft__labels"
|
||||
placeholder=${t("workboard.labelsPlaceholder")}
|
||||
?disabled=${draftActionsBusy}
|
||||
.value=${state.draftLabels}
|
||||
@input=${(event: InputEvent) => {
|
||||
state.draftLabels = (event.currentTarget as HTMLInputElement).value;
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -339,14 +368,10 @@ export function renderCardModal(props: WorkboardProps) {
|
||||
maxlength="2000"
|
||||
?disabled=${draftActionsBusy}
|
||||
.value=${state.draftCommentBody}
|
||||
@input=${(event: InputEvent) => {
|
||||
state.draftCommentBody = (event.currentTarget as HTMLTextAreaElement).value;
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
></textarea>
|
||||
<div class="workboard-modal__actions">
|
||||
<button
|
||||
class="btn"
|
||||
class="btn workboard-comments__submit"
|
||||
type="button"
|
||||
?disabled=${draftActionsBusy || !state.draftCommentBody.trim()}
|
||||
@click=${() => {
|
||||
@@ -365,7 +390,10 @@ export function renderCardModal(props: WorkboardProps) {
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="workboard-modal__actions">
|
||||
<button class="btn primary" ?disabled=${draftActionsBusy || !state.draftTitle.trim()}>
|
||||
<button
|
||||
class="btn primary workboard-draft__submit"
|
||||
?disabled=${draftActionsBusy || !state.draftTitle.trim()}
|
||||
>
|
||||
${editing ? t("common.save") : t("common.create")}
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -105,6 +105,16 @@ async function chooseWorkboardSelectFieldOption(
|
||||
}, optionValue);
|
||||
}
|
||||
|
||||
async function setWorkboardDraftField(
|
||||
scope: Page | Locator,
|
||||
label: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
const input = scope.getByLabel(label);
|
||||
await input.fill(value);
|
||||
await expect.poll(() => input.inputValue()).toBe(value);
|
||||
}
|
||||
|
||||
async function waitForRequests(
|
||||
gateway: MockGatewayControls,
|
||||
method: string,
|
||||
@@ -440,10 +450,10 @@ describeControlUiE2e("Control UI Workboard mocked Gateway E2E", () => {
|
||||
const createDialog = writable.page.getByRole("dialog", { name: "New card" });
|
||||
const createForm = writable.page.locator('openclaw-modal-dialog[label="New card"]');
|
||||
await expect.poll(() => createDialog.isVisible()).toBe(true);
|
||||
await createForm.getByLabel("Title").fill(createdCard.title);
|
||||
await createForm.getByLabel("Notes").fill(createdCard.notes ?? "");
|
||||
await setWorkboardDraftField(createForm, "Title", createdCard.title);
|
||||
await setWorkboardDraftField(createForm, "Notes", createdCard.notes ?? "");
|
||||
await chooseWorkboardSelectOption(createForm, "Thread", linkedSessionName);
|
||||
await createForm.getByLabel("Labels").fill("ui, proof");
|
||||
await setWorkboardDraftField(createForm, "Labels", "ui, proof");
|
||||
await captureScreenshot(writable.page, artifacts, "02-create-dialog");
|
||||
const createBefore = (await writableGateway.getRequests("workboard.cards.create")).length;
|
||||
await createForm.getByRole("button", { name: /^Create$/u }).click();
|
||||
@@ -495,10 +505,10 @@ describeControlUiE2e("Control UI Workboard mocked Gateway E2E", () => {
|
||||
const editDialog = writable.page.getByRole("dialog", { name: "Edit card" });
|
||||
const editForm = writable.page.locator('openclaw-modal-dialog[label="Edit card"]');
|
||||
await expect.poll(() => editDialog.isVisible()).toBe(true);
|
||||
await editForm.getByLabel("Title").fill(editedCard.title);
|
||||
await editForm.getByLabel("Notes").fill(editedCard.notes ?? "");
|
||||
await setWorkboardDraftField(editForm, "Title", editedCard.title);
|
||||
await setWorkboardDraftField(editForm, "Notes", editedCard.notes ?? "");
|
||||
await chooseWorkboardSelectOption(editForm, "Priority", "High");
|
||||
await editForm.getByLabel("Labels").fill("ui, proof, e2e");
|
||||
await setWorkboardDraftField(editForm, "Labels", "ui, proof, e2e");
|
||||
const updateBeforeEdit = (await writableGateway.getRequests("workboard.cards.update")).length;
|
||||
await editForm.getByRole("button", { name: /^Save$/u }).click();
|
||||
const editRequest = await waitForNextRequest(
|
||||
|
||||
Reference in New Issue
Block a user