mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(ui): show Workboard comments in edit modal
Show existing Workboard card comments in the edit modal and allow operators to append a new comment through the existing `workboard.cards.comment` gateway method. Refs #88592. Verification: - node scripts/run-vitest.mjs ui/src/ui/views/workboard.test.ts - pnpm tsgo:test:ui - git diff --check origin/main...HEAD - .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main Co-authored-by: Ted Li <tl2493@columbia.edu>
This commit is contained in:
@@ -310,6 +310,7 @@ export type WorkboardUiState = {
|
||||
draftAgentId: string;
|
||||
draftSessionKey: string;
|
||||
draftTemplateId: WorkboardTemplateId | "";
|
||||
draftCommentBody: string;
|
||||
busyCardId: string | null;
|
||||
draggedCardId: string | null;
|
||||
syncingCardIds: Set<string>;
|
||||
@@ -352,6 +353,7 @@ function createDefaultState(): WorkboardUiState {
|
||||
draftAgentId: "",
|
||||
draftSessionKey: "",
|
||||
draftTemplateId: "",
|
||||
draftCommentBody: "",
|
||||
busyCardId: null,
|
||||
draggedCardId: null,
|
||||
syncingCardIds: new Set(),
|
||||
@@ -947,6 +949,7 @@ function resetDraftState(state: WorkboardUiState) {
|
||||
state.draftAgentId = "";
|
||||
state.draftSessionKey = "";
|
||||
state.draftTemplateId = "";
|
||||
state.draftCommentBody = "";
|
||||
}
|
||||
|
||||
function normalizeDraftLabels(value: string): string[] {
|
||||
@@ -1418,6 +1421,34 @@ export async function saveWorkboardCardDraft(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function addWorkboardCardComment(params: {
|
||||
host: WorkboardHost;
|
||||
client: GatewayBrowserClient | null;
|
||||
requestUpdate?: () => void;
|
||||
}) {
|
||||
const state = getWorkboardState(params.host);
|
||||
const body = state.draftCommentBody.trim();
|
||||
if (!state.editingCardId || !params.client || !body) {
|
||||
return;
|
||||
}
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
params.requestUpdate?.();
|
||||
try {
|
||||
const payload = await params.client.request("workboard.cards.comment", {
|
||||
id: state.editingCardId,
|
||||
body,
|
||||
});
|
||||
replaceCard(state, normalizeCardPayload(payload));
|
||||
state.draftCommentBody = "";
|
||||
} catch (error) {
|
||||
state.error = formatError(error);
|
||||
} finally {
|
||||
state.loading = false;
|
||||
params.requestUpdate?.();
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveWorkboardCard(params: {
|
||||
host: WorkboardHost;
|
||||
client: GatewayBrowserClient | null;
|
||||
|
||||
@@ -609,16 +609,33 @@ describe("renderWorkboard", () => {
|
||||
position: 1000,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
metadata: {
|
||||
comments: [{ id: "comment-1", body: "Needs owner check", createdAt: 2 }],
|
||||
},
|
||||
},
|
||||
];
|
||||
const request = vi.fn(async () => ({
|
||||
card: {
|
||||
...state.cards[0],
|
||||
title: "Renamed",
|
||||
priority: "high",
|
||||
updatedAt: 2,
|
||||
},
|
||||
}));
|
||||
const request = vi.fn(async (method: string) =>
|
||||
method === "workboard.cards.comment"
|
||||
? {
|
||||
card: {
|
||||
...state.cards[0],
|
||||
metadata: {
|
||||
comments: [
|
||||
...(state.cards[0]?.metadata?.comments ?? []),
|
||||
{ id: "comment-2", body: "Ship after CI", createdAt: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
card: {
|
||||
...state.cards[0],
|
||||
title: "Renamed",
|
||||
priority: "high",
|
||||
updatedAt: 2,
|
||||
},
|
||||
},
|
||||
);
|
||||
const props = {
|
||||
host,
|
||||
client: { request } as unknown as GatewayBrowserClient,
|
||||
@@ -638,6 +655,24 @@ describe("renderWorkboard", () => {
|
||||
render(renderWorkboard(props), container);
|
||||
|
||||
expect(container.querySelector('[role="dialog"]')?.textContent).toContain("Edit card");
|
||||
expect(container.querySelector('[role="dialog"]')?.textContent).toContain("Needs owner check");
|
||||
const commentInput = container.querySelector<HTMLTextAreaElement>(".workboard-comments__input");
|
||||
commentInput!.value = "Ship after CI";
|
||||
commentInput!.dispatchEvent(new InputEvent("input", { bubbles: true }));
|
||||
render(renderWorkboard(props), container);
|
||||
[...container.querySelectorAll<HTMLButtonElement>("button")]
|
||||
.find((button) => button.textContent?.includes("Create"))
|
||||
?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(request).toHaveBeenCalledWith("workboard.cards.comment", {
|
||||
id: "card-1",
|
||||
body: "Ship after CI",
|
||||
});
|
||||
expect(state.cards[0]?.metadata?.comments?.at(-1)?.body).toBe("Ship after CI");
|
||||
render(renderWorkboard(props), container);
|
||||
|
||||
const title = container.querySelector<HTMLInputElement>(".workboard-draft__title");
|
||||
expect(title?.value).toBe("Rename me");
|
||||
title!.value = "Renamed";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import {
|
||||
addWorkboardCardComment,
|
||||
archiveWorkboardCard,
|
||||
deleteWorkboardCard,
|
||||
dispatchWorkboard,
|
||||
@@ -352,6 +353,7 @@ function resetDraft(state: WorkboardUiState) {
|
||||
state.draftAgentId = "";
|
||||
state.draftSessionKey = "";
|
||||
state.draftTemplateId = "";
|
||||
state.draftCommentBody = "";
|
||||
}
|
||||
|
||||
function openCreateModal(state: WorkboardUiState) {
|
||||
@@ -405,6 +407,7 @@ function openEditModal(state: WorkboardUiState, card: WorkboardCard) {
|
||||
state.draftAgentId = card.agentId ?? "";
|
||||
state.draftSessionKey = card.sessionKey ?? "";
|
||||
state.draftTemplateId = card.metadata?.templateId ?? "";
|
||||
state.draftCommentBody = "";
|
||||
}
|
||||
|
||||
function applyTemplate(state: WorkboardUiState, templateId: WorkboardTemplateId) {
|
||||
@@ -569,6 +572,10 @@ function renderCardModal(props: WorkboardProps) {
|
||||
return nothing;
|
||||
}
|
||||
const editing = Boolean(state.editingCardId);
|
||||
const editingCard = state.editingCardId
|
||||
? (state.cards.find((card) => card.id === state.editingCardId) ?? null)
|
||||
: null;
|
||||
const comments = editingCard?.metadata?.comments ?? [];
|
||||
return html`
|
||||
<div
|
||||
class="workboard-modal"
|
||||
@@ -745,6 +752,51 @@ function renderCardModal(props: WorkboardProps) {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
${editing
|
||||
? html`
|
||||
<section
|
||||
class="workboard-field workboard-field--wide"
|
||||
aria-labelledby="workboard-card-comments-title"
|
||||
>
|
||||
<span id="workboard-card-comments-title">
|
||||
${t("workboard.badgeComments", { count: String(comments.length) })}
|
||||
</span>
|
||||
${comments.length
|
||||
? html`
|
||||
<ol>
|
||||
${comments.map((comment) => html`<li>${comment.body}</li>`)}
|
||||
</ol>
|
||||
`
|
||||
: nothing}
|
||||
<textarea
|
||||
class="input workboard-comments__input"
|
||||
aria-labelledby="workboard-card-comments-title"
|
||||
maxlength="2000"
|
||||
.value=${state.draftCommentBody}
|
||||
@input=${(event: InputEvent) => {
|
||||
state.draftCommentBody = (event.currentTarget as HTMLTextAreaElement).value;
|
||||
props.onRequestUpdate?.();
|
||||
}}
|
||||
></textarea>
|
||||
<div class="workboard-modal__actions">
|
||||
<button
|
||||
class="btn"
|
||||
type="button"
|
||||
?disabled=${state.loading || !state.draftCommentBody.trim()}
|
||||
@click=${() => {
|
||||
void addWorkboardCardComment({
|
||||
host: props.host,
|
||||
client: props.client,
|
||||
requestUpdate: props.onRequestUpdate,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${icons.plus} ${t("common.create")}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
: nothing}
|
||||
<div class="workboard-modal__actions">
|
||||
<button class="btn primary" ?disabled=${state.loading || !state.draftTitle.trim()}>
|
||||
${editing ? t("common.save") : t("common.create")}
|
||||
|
||||
Reference in New Issue
Block a user