Q146: the message editor stays open until the edit is saved, and says why it was not
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8a06f2ff77
commit
02309546dc
6 files changed
+40
-9
No files matched your search
@@ -37,6 +37,7 @@ export const zh: Record<string, string> = {
|
||||
"Waiting on you, or resumes on its own at {time}.": "等待你处理,或于 {time} 自动恢复。",
|
||||
"Waiting on you.": "等待你处理。",
|
||||
"Not sent: {reason}": "未发送:{reason}",
|
||||
"Not saved: {reason}": "未保存:{reason}",
|
||||
"Remove {name}": "移除 {name}",
|
||||
"Not attached: {reason}": "未能附加:{reason}",
|
||||
"Reply in thread": "在话题中回复",
|
||||
|
||||
@@ -180,14 +180,15 @@ export function ChatPage({ snapshot, actions }: PageProps) {
|
||||
decidedByEmployeeId: employee.id,
|
||||
}));
|
||||
|
||||
const editChatMessage = (messageId: string, bodyMd: string) =>
|
||||
void actions.run(() => window.agentCompany.editMessage({
|
||||
const editChatMessage = async (messageId: string, bodyMd: string) => {
|
||||
await actions.run(() => window.agentCompany.editMessage({
|
||||
orgId,
|
||||
messageId,
|
||||
actorType: "employee",
|
||||
actorId: employee.id,
|
||||
bodyMd,
|
||||
}));
|
||||
};
|
||||
|
||||
const deleteChatMessage = (messageId: string) =>
|
||||
void actions.run(() => window.agentCompany.deleteMessage({
|
||||
|
||||
@@ -69,7 +69,7 @@ export function ChatConversation({
|
||||
onAttach: () => void;
|
||||
onApprove: (approvalId: string) => void;
|
||||
onDecline: (approvalId: string) => void;
|
||||
onEditMessage: (messageId: string, bodyMd: string) => void;
|
||||
onEditMessage: (messageId: string, bodyMd: string) => Promise<void>;
|
||||
onDeleteMessage: (messageId: string) => void;
|
||||
onOpenFile: (fileId: string) => void;
|
||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||
|
||||
@@ -41,11 +41,14 @@ export function ChatMessageRow({
|
||||
/** Where each agent's reply to this message stands; a finished reply shows nothing. */
|
||||
replyStatuses?: ChatReplyStatus[];
|
||||
onOpenThread?: () => void;
|
||||
onEdit?: (bodyMd: string) => void;
|
||||
/** Saves an edit; the editor stays open until it is saved. */
|
||||
onEdit?: (bodyMd: string) => Promise<void>;
|
||||
onDelete?: () => void;
|
||||
onOpenFile?: (fileId: string) => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [editError, setEditError] = useState<string | null>(null);
|
||||
const name = senderName(message, context);
|
||||
const bot = message.senderType === "agent";
|
||||
const deleted = Boolean(message.deletedAt);
|
||||
@@ -57,8 +60,19 @@ export function ChatMessageRow({
|
||||
event.preventDefault();
|
||||
const next = new FormData(event.currentTarget).get("body");
|
||||
const value = typeof next === "string" ? next.trim() : "";
|
||||
if (value && onEdit) onEdit(value);
|
||||
setEditing(false);
|
||||
if (!value || !onEdit || saving) return;
|
||||
setSaving(true);
|
||||
setEditError(null);
|
||||
// The editor closes only once the edit is saved; a refusal keeps the text and says why.
|
||||
onEdit(value).then(() => {
|
||||
setSaving(false);
|
||||
setEditing(false);
|
||||
}).catch((reason: unknown) => {
|
||||
const message = reason instanceof Error ? reason.message : String(reason);
|
||||
console.error(`[chat] edit failed: ${message}`);
|
||||
setSaving(false);
|
||||
setEditError(message);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="chat-message" style={{ display: "flex", gap: 12, marginBottom: 18 }}>
|
||||
@@ -102,12 +116,14 @@ export function ChatMessageRow({
|
||||
defaultValue={message.bodyMd}
|
||||
autoFocus
|
||||
aria-label={t("Edit message")}
|
||||
disabled={saving}
|
||||
className="input"
|
||||
style={{ minHeight: 60, marginBottom: 6 }}
|
||||
/>
|
||||
{editError && <div className="form-error" role="alert" style={{ marginBottom: 6 }}>{t("Not saved: {reason}", { reason: editError })}</div>}
|
||||
<div className="row" style={{ gap: 6 }}>
|
||||
<Button tone="primary" size="small" type="submit">{t("Save")}</Button>
|
||||
<Button tone="ghost" size="small" onClick={() => setEditing(false)}>{t("Cancel")}</Button>
|
||||
<Button tone="primary" size="small" type="submit" disabled={saving}>{t("Save")}</Button>
|
||||
<Button tone="ghost" size="small" disabled={saving} onClick={() => { setEditing(false); setEditError(null); }}>{t("Cancel")}</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
|
||||
@@ -68,7 +68,7 @@ export function ChatTimeline({
|
||||
onOpenThread: (threadId: string) => void;
|
||||
onApprove: (approvalId: string) => void;
|
||||
onDecline: (approvalId: string) => void;
|
||||
onEditMessage: (messageId: string, bodyMd: string) => void;
|
||||
onEditMessage: (messageId: string, bodyMd: string) => Promise<void>;
|
||||
onDeleteMessage: (messageId: string) => void;
|
||||
onOpenFile: (fileId: string) => void;
|
||||
}) {
|
||||
|
||||
@@ -111,6 +111,19 @@ test("the pinned CEO conversation is the Messages front door", async () => {
|
||||
await expect(composer).toHaveValue("");
|
||||
await expect(page.getByRole("alert")).toHaveCount(0);
|
||||
|
||||
// An edit the backend refuses keeps the editor open with the text and says why.
|
||||
const own = chatLog.locator(".chat-message", { hasText: "Second thought: what is blocked?" });
|
||||
await own.getByRole("button", { name: "Edit message" }).click();
|
||||
const editor = own.getByRole("textbox", { name: "Edit message" });
|
||||
await editor.fill("/status everything");
|
||||
await own.getByRole("button", { name: "Save" }).click();
|
||||
await expect(own.getByRole("alert")).toContainText("Not saved: Slash command messages are immutable.");
|
||||
await expect(editor).toHaveValue("/status everything");
|
||||
await editor.fill("Second thought: what is blocked right now?");
|
||||
await own.getByRole("button", { name: "Save" }).click();
|
||||
await expect(chatLog.getByText("Second thought: what is blocked right now?")).toBeVisible();
|
||||
await expect(chatLog.getByRole("textbox", { name: "Edit message" })).toHaveCount(0);
|
||||
|
||||
// Each conversation keeps its own unsent draft.
|
||||
await composer.fill("Draft for the CEO only");
|
||||
await page.locator(".convo-item", { hasText: "general" }).click();
|
||||
|
||||
Reference in new issue
Block a user