@consilioweb/payload-support 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/RichTextEditor/index.cjs +47 -1
- package/dist/components/RichTextEditor/index.js +47 -1
- package/dist/views/TicketDetailView/client.cjs +48 -2
- package/dist/views/TicketDetailView/client.js +48 -2
- package/package.json +1 -1
- package/src/components/RichTextEditor/index.tsx +48 -0
- package/src/views/TicketDetailView/client.tsx +59 -8
|
@@ -99,6 +99,43 @@ const RichTextEditor = react.forwardRef(function RichTextEditor2({
|
|
|
99
99
|
}
|
|
100
100
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
101
101
|
}, [onFileUpload, emitChange]);
|
|
102
|
+
const handleClearFormat = react.useCallback(() => {
|
|
103
|
+
document.execCommand("removeFormat");
|
|
104
|
+
document.execCommand("formatBlock", false, "p");
|
|
105
|
+
editorRef.current?.focus();
|
|
106
|
+
setTimeout(emitChange, 0);
|
|
107
|
+
}, [emitChange]);
|
|
108
|
+
const handleKeyDown = react.useCallback((e) => {
|
|
109
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
110
|
+
const sel = window.getSelection();
|
|
111
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
112
|
+
let node = sel.anchorNode;
|
|
113
|
+
let blockquote = null;
|
|
114
|
+
while (node && node !== editorRef.current) {
|
|
115
|
+
if (node.nodeName === "BLOCKQUOTE") {
|
|
116
|
+
blockquote = node;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
node = node.parentNode;
|
|
120
|
+
}
|
|
121
|
+
if (!blockquote) return;
|
|
122
|
+
const text = blockquote.innerText || "";
|
|
123
|
+
if (text.replace(/\n+$/, "").length === 0 || text.endsWith("\n\n") || text.endsWith("\n")) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
const p = document.createElement("p");
|
|
126
|
+
p.innerHTML = "<br>";
|
|
127
|
+
blockquote.after(p);
|
|
128
|
+
if (blockquote.lastChild && blockquote.lastChild.nodeName === "BR") {
|
|
129
|
+
blockquote.removeChild(blockquote.lastChild);
|
|
130
|
+
}
|
|
131
|
+
const range = document.createRange();
|
|
132
|
+
range.setStart(p, 0);
|
|
133
|
+
range.collapse(true);
|
|
134
|
+
sel.removeAllRanges();
|
|
135
|
+
sel.addRange(range);
|
|
136
|
+
setTimeout(emitChange, 0);
|
|
137
|
+
}
|
|
138
|
+
}, [emitChange]);
|
|
102
139
|
const handlePaste = react.useCallback(async (e) => {
|
|
103
140
|
if (!onFileUpload) return;
|
|
104
141
|
const items = e.clipboardData?.items;
|
|
@@ -186,7 +223,15 @@ const RichTextEditor = react.forwardRef(function RichTextEditor2({
|
|
|
186
223
|
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: btn, onMouseDown: (e) => {
|
|
187
224
|
e.preventDefault();
|
|
188
225
|
handleImageClick();
|
|
189
|
-
}, title: "Ins\xE9rer une image", children: "\u{1F5BC}\uFE0F" })
|
|
226
|
+
}, title: "Ins\xE9rer une image", children: "\u{1F5BC}\uFE0F" }),
|
|
227
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: sep }),
|
|
228
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", style: { ...btn, fontSize: "13px" }, onMouseDown: (e) => {
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
handleClearFormat();
|
|
231
|
+
}, title: "Effacer la mise en forme", children: [
|
|
232
|
+
"T",
|
|
233
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "10px", verticalAlign: "super" }, children: "\xD7" })
|
|
234
|
+
] })
|
|
190
235
|
] }),
|
|
191
236
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative" }, children: [
|
|
192
237
|
isEmpty && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", top: 0, left: 0, right: 0, padding: "14px 16px", color: "#9ca3af", fontSize: "14px", pointerEvents: "none", userSelect: "none" }, children: placeholder }),
|
|
@@ -204,6 +249,7 @@ const RichTextEditor = react.forwardRef(function RichTextEditor2({
|
|
|
204
249
|
emitChange();
|
|
205
250
|
},
|
|
206
251
|
onPaste: handlePaste,
|
|
252
|
+
onKeyDown: handleKeyDown,
|
|
207
253
|
style: {
|
|
208
254
|
minHeight: `${minHeight}px`,
|
|
209
255
|
padding: "14px 16px",
|
|
@@ -98,6 +98,43 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
|
|
|
98
98
|
}
|
|
99
99
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
100
100
|
}, [onFileUpload, emitChange]);
|
|
101
|
+
const handleClearFormat = useCallback(() => {
|
|
102
|
+
document.execCommand("removeFormat");
|
|
103
|
+
document.execCommand("formatBlock", false, "p");
|
|
104
|
+
editorRef.current?.focus();
|
|
105
|
+
setTimeout(emitChange, 0);
|
|
106
|
+
}, [emitChange]);
|
|
107
|
+
const handleKeyDown = useCallback((e) => {
|
|
108
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
109
|
+
const sel = window.getSelection();
|
|
110
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
111
|
+
let node = sel.anchorNode;
|
|
112
|
+
let blockquote = null;
|
|
113
|
+
while (node && node !== editorRef.current) {
|
|
114
|
+
if (node.nodeName === "BLOCKQUOTE") {
|
|
115
|
+
blockquote = node;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
node = node.parentNode;
|
|
119
|
+
}
|
|
120
|
+
if (!blockquote) return;
|
|
121
|
+
const text = blockquote.innerText || "";
|
|
122
|
+
if (text.replace(/\n+$/, "").length === 0 || text.endsWith("\n\n") || text.endsWith("\n")) {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
const p = document.createElement("p");
|
|
125
|
+
p.innerHTML = "<br>";
|
|
126
|
+
blockquote.after(p);
|
|
127
|
+
if (blockquote.lastChild && blockquote.lastChild.nodeName === "BR") {
|
|
128
|
+
blockquote.removeChild(blockquote.lastChild);
|
|
129
|
+
}
|
|
130
|
+
const range = document.createRange();
|
|
131
|
+
range.setStart(p, 0);
|
|
132
|
+
range.collapse(true);
|
|
133
|
+
sel.removeAllRanges();
|
|
134
|
+
sel.addRange(range);
|
|
135
|
+
setTimeout(emitChange, 0);
|
|
136
|
+
}
|
|
137
|
+
}, [emitChange]);
|
|
101
138
|
const handlePaste = useCallback(async (e) => {
|
|
102
139
|
if (!onFileUpload) return;
|
|
103
140
|
const items = e.clipboardData?.items;
|
|
@@ -185,7 +222,15 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
|
|
|
185
222
|
/* @__PURE__ */ jsx("button", { type: "button", style: btn, onMouseDown: (e) => {
|
|
186
223
|
e.preventDefault();
|
|
187
224
|
handleImageClick();
|
|
188
|
-
}, title: "Ins\xE9rer une image", children: "\u{1F5BC}\uFE0F" })
|
|
225
|
+
}, title: "Ins\xE9rer une image", children: "\u{1F5BC}\uFE0F" }),
|
|
226
|
+
/* @__PURE__ */ jsx("span", { style: sep }),
|
|
227
|
+
/* @__PURE__ */ jsxs("button", { type: "button", style: { ...btn, fontSize: "13px" }, onMouseDown: (e) => {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
handleClearFormat();
|
|
230
|
+
}, title: "Effacer la mise en forme", children: [
|
|
231
|
+
"T",
|
|
232
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: "10px", verticalAlign: "super" }, children: "\xD7" })
|
|
233
|
+
] })
|
|
189
234
|
] }),
|
|
190
235
|
/* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
|
|
191
236
|
isEmpty && /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: 0, left: 0, right: 0, padding: "14px 16px", color: "#9ca3af", fontSize: "14px", pointerEvents: "none", userSelect: "none" }, children: placeholder }),
|
|
@@ -203,6 +248,7 @@ const RichTextEditor = forwardRef(function RichTextEditor2({
|
|
|
203
248
|
emitChange();
|
|
204
249
|
},
|
|
205
250
|
onPaste: handlePaste,
|
|
251
|
+
onKeyDown: handleKeyDown,
|
|
206
252
|
style: {
|
|
207
253
|
minHeight: `${minHeight}px`,
|
|
208
254
|
padding: "14px 16px",
|
|
@@ -139,6 +139,9 @@ const TicketDetailClient = () => {
|
|
|
139
139
|
const [isInternal, setIsInternal] = React.useState(false);
|
|
140
140
|
const [notifyClient, setNotifyClient] = React.useState(true);
|
|
141
141
|
const [sendAsClient, setSendAsClient] = React.useState(false);
|
|
142
|
+
const [editingMsgId, setEditingMsgId] = React.useState(null);
|
|
143
|
+
const [editingBody, setEditingBody] = React.useState("");
|
|
144
|
+
const [editSaving, setEditSaving] = React.useState(false);
|
|
142
145
|
const [sending, setSending] = React.useState(false);
|
|
143
146
|
const [showMenu, setShowMenu] = React.useState(false);
|
|
144
147
|
const [clientTyping, setClientTyping] = React.useState(false);
|
|
@@ -477,6 +480,34 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
477
480
|
setUndoToast(null);
|
|
478
481
|
}
|
|
479
482
|
};
|
|
483
|
+
const startEditMessage = (msg) => {
|
|
484
|
+
setEditingMsgId(msg.id);
|
|
485
|
+
setEditingBody(msg.body || "");
|
|
486
|
+
};
|
|
487
|
+
const cancelEditMessage = () => {
|
|
488
|
+
setEditingMsgId(null);
|
|
489
|
+
setEditingBody("");
|
|
490
|
+
};
|
|
491
|
+
const saveEditMessage = async () => {
|
|
492
|
+
if (editingMsgId === null) return;
|
|
493
|
+
setEditSaving(true);
|
|
494
|
+
try {
|
|
495
|
+
const res = await fetch(`/api/ticket-messages/${editingMsgId}`, {
|
|
496
|
+
method: "PATCH",
|
|
497
|
+
headers: { "Content-Type": "application/json" },
|
|
498
|
+
credentials: "include",
|
|
499
|
+
body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true })
|
|
500
|
+
});
|
|
501
|
+
if (res.ok) {
|
|
502
|
+
setEditingMsgId(null);
|
|
503
|
+
setEditingBody("");
|
|
504
|
+
fetchAll();
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
} finally {
|
|
508
|
+
setEditSaving(false);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
480
511
|
const handleSplitConfirm = async () => {
|
|
481
512
|
if (!splitModal || !splitSubject.trim()) return;
|
|
482
513
|
try {
|
|
@@ -686,7 +717,21 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
686
717
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#94a3b8" }, children: "\u2713" });
|
|
687
718
|
})() })
|
|
688
719
|
] }),
|
|
689
|
-
msg.
|
|
720
|
+
editingMsgId === msg.id ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
|
|
721
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
722
|
+
"textarea",
|
|
723
|
+
{
|
|
724
|
+
value: editingBody,
|
|
725
|
+
onChange: (e) => setEditingBody(e.target.value),
|
|
726
|
+
style: { width: "100%", minHeight: 120, padding: 10, fontSize: 13, lineHeight: 1.5, fontFamily: "inherit", border: "1px solid var(--theme-elevation-200)", borderRadius: 6, background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
|
|
727
|
+
autoFocus: true
|
|
728
|
+
}
|
|
729
|
+
),
|
|
730
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, justifyContent: "flex-end" }, children: [
|
|
731
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: cancelEditMessage, disabled: editSaving, style: { padding: "5px 12px", fontSize: 12, borderRadius: 5, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", color: "var(--theme-text)", cursor: "pointer" }, children: "Annuler" }),
|
|
732
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: saveEditMessage, disabled: editSaving || !editingBody.trim(), style: { padding: "5px 12px", fontSize: 12, fontWeight: 600, borderRadius: 5, border: "none", background: "#2563eb", color: "#fff", cursor: editSaving ? "wait" : "pointer", opacity: editSaving ? 0.6 : 1 }, children: editSaving ? "Sauvegarde\u2026" : "Enregistrer" })
|
|
733
|
+
] })
|
|
734
|
+
] }) : msg.deletedAt ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.messageBody, style: { color: "#94a3b8", fontStyle: "italic" }, children: t("detail.messageDeleted") }) : msg.bodyHtml && CodeBlock.hasCodeBlocks(msg.bodyHtml.replace(/<[^>]+>/g, "")) ? /* @__PURE__ */ jsxRuntime.jsx(CodeBlock.CodeBlockRendererHtml, { html: msg.bodyHtml }) : msg.bodyHtml ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${s__default.default.messageBody} ${s__default.default.rteDisplay}`, dangerouslySetInnerHTML: { __html: msg.bodyHtml } }) : CodeBlock.hasCodeBlocks(msg.body) ? /* @__PURE__ */ jsxRuntime.jsx(CodeBlock.MessageWithCodeBlocks, { text: msg.body, style: { fontSize: "13px", lineHeight: 1.5 } }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.messageBody, children: msg.body }),
|
|
690
735
|
Array.isArray(msg.attachments) && msg.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.attachments, children: msg.attachments.map((att, i) => {
|
|
691
736
|
const file = typeof att.file === "object" ? att.file : null;
|
|
692
737
|
if (!file) return null;
|
|
@@ -696,7 +741,8 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
696
741
|
] }, i);
|
|
697
742
|
}) })
|
|
698
743
|
] }),
|
|
699
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.messageActions, children: [
|
|
744
|
+
editingMsgId !== msg.id && !msg.deletedAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.messageActions, children: [
|
|
745
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.actionIcon, title: "\xC9diter", "aria-label": "\xC9diter le message", onClick: () => startEditMessage(msg), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: "\xC9diter" }),
|
|
700
746
|
/* @__PURE__ */ jsxRuntime.jsx("button", { className: `${s__default.default.actionIcon} ${s__default.default.danger}`, title: t("actions.deleteMessage"), "aria-label": t("actions.deleteMessage"), onClick: () => handleDeleteMessage(msg.id), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: t("actions.deleteMessage") }),
|
|
701
747
|
features.splitTicket && !msg.isInternal && /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.actionIcon, title: t("actions.extractMessage"), "aria-label": t("actions.extractToNewTicket"), onClick: () => {
|
|
702
748
|
setSplitModal({ messageId: msg.id, preview: msg.body.slice(0, 200) });
|
|
@@ -132,6 +132,9 @@ const TicketDetailClient = () => {
|
|
|
132
132
|
const [isInternal, setIsInternal] = useState(false);
|
|
133
133
|
const [notifyClient, setNotifyClient] = useState(true);
|
|
134
134
|
const [sendAsClient, setSendAsClient] = useState(false);
|
|
135
|
+
const [editingMsgId, setEditingMsgId] = useState(null);
|
|
136
|
+
const [editingBody, setEditingBody] = useState("");
|
|
137
|
+
const [editSaving, setEditSaving] = useState(false);
|
|
135
138
|
const [sending, setSending] = useState(false);
|
|
136
139
|
const [showMenu, setShowMenu] = useState(false);
|
|
137
140
|
const [clientTyping, setClientTyping] = useState(false);
|
|
@@ -470,6 +473,34 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
470
473
|
setUndoToast(null);
|
|
471
474
|
}
|
|
472
475
|
};
|
|
476
|
+
const startEditMessage = (msg) => {
|
|
477
|
+
setEditingMsgId(msg.id);
|
|
478
|
+
setEditingBody(msg.body || "");
|
|
479
|
+
};
|
|
480
|
+
const cancelEditMessage = () => {
|
|
481
|
+
setEditingMsgId(null);
|
|
482
|
+
setEditingBody("");
|
|
483
|
+
};
|
|
484
|
+
const saveEditMessage = async () => {
|
|
485
|
+
if (editingMsgId === null) return;
|
|
486
|
+
setEditSaving(true);
|
|
487
|
+
try {
|
|
488
|
+
const res = await fetch(`/api/ticket-messages/${editingMsgId}`, {
|
|
489
|
+
method: "PATCH",
|
|
490
|
+
headers: { "Content-Type": "application/json" },
|
|
491
|
+
credentials: "include",
|
|
492
|
+
body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true })
|
|
493
|
+
});
|
|
494
|
+
if (res.ok) {
|
|
495
|
+
setEditingMsgId(null);
|
|
496
|
+
setEditingBody("");
|
|
497
|
+
fetchAll();
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
} finally {
|
|
501
|
+
setEditSaving(false);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
473
504
|
const handleSplitConfirm = async () => {
|
|
474
505
|
if (!splitModal || !splitSubject.trim()) return;
|
|
475
506
|
try {
|
|
@@ -679,7 +710,21 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
679
710
|
return /* @__PURE__ */ jsx("span", { style: { color: "#94a3b8" }, children: "\u2713" });
|
|
680
711
|
})() })
|
|
681
712
|
] }),
|
|
682
|
-
msg.
|
|
713
|
+
editingMsgId === msg.id ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
|
|
714
|
+
/* @__PURE__ */ jsx(
|
|
715
|
+
"textarea",
|
|
716
|
+
{
|
|
717
|
+
value: editingBody,
|
|
718
|
+
onChange: (e) => setEditingBody(e.target.value),
|
|
719
|
+
style: { width: "100%", minHeight: 120, padding: 10, fontSize: 13, lineHeight: 1.5, fontFamily: "inherit", border: "1px solid var(--theme-elevation-200)", borderRadius: 6, background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
|
|
720
|
+
autoFocus: true
|
|
721
|
+
}
|
|
722
|
+
),
|
|
723
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, justifyContent: "flex-end" }, children: [
|
|
724
|
+
/* @__PURE__ */ jsx("button", { onClick: cancelEditMessage, disabled: editSaving, style: { padding: "5px 12px", fontSize: 12, borderRadius: 5, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", color: "var(--theme-text)", cursor: "pointer" }, children: "Annuler" }),
|
|
725
|
+
/* @__PURE__ */ jsx("button", { onClick: saveEditMessage, disabled: editSaving || !editingBody.trim(), style: { padding: "5px 12px", fontSize: 12, fontWeight: 600, borderRadius: 5, border: "none", background: "#2563eb", color: "#fff", cursor: editSaving ? "wait" : "pointer", opacity: editSaving ? 0.6 : 1 }, children: editSaving ? "Sauvegarde\u2026" : "Enregistrer" })
|
|
726
|
+
] })
|
|
727
|
+
] }) : msg.deletedAt ? /* @__PURE__ */ jsx("div", { className: s.messageBody, style: { color: "#94a3b8", fontStyle: "italic" }, children: t("detail.messageDeleted") }) : msg.bodyHtml && hasCodeBlocks(msg.bodyHtml.replace(/<[^>]+>/g, "")) ? /* @__PURE__ */ jsx(CodeBlockRendererHtml, { html: msg.bodyHtml }) : msg.bodyHtml ? /* @__PURE__ */ jsx("div", { className: `${s.messageBody} ${s.rteDisplay}`, dangerouslySetInnerHTML: { __html: msg.bodyHtml } }) : hasCodeBlocks(msg.body) ? /* @__PURE__ */ jsx(MessageWithCodeBlocks, { text: msg.body, style: { fontSize: "13px", lineHeight: 1.5 } }) : /* @__PURE__ */ jsx("div", { className: s.messageBody, children: msg.body }),
|
|
683
728
|
Array.isArray(msg.attachments) && msg.attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: s.attachments, children: msg.attachments.map((att, i) => {
|
|
684
729
|
const file = typeof att.file === "object" ? att.file : null;
|
|
685
730
|
if (!file) return null;
|
|
@@ -689,7 +734,8 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
689
734
|
] }, i);
|
|
690
735
|
}) })
|
|
691
736
|
] }),
|
|
692
|
-
/* @__PURE__ */ jsxs("div", { className: s.messageActions, children: [
|
|
737
|
+
editingMsgId !== msg.id && !msg.deletedAt && /* @__PURE__ */ jsxs("div", { className: s.messageActions, children: [
|
|
738
|
+
/* @__PURE__ */ jsx("button", { className: s.actionIcon, title: "\xC9diter", "aria-label": "\xC9diter le message", onClick: () => startEditMessage(msg), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: "\xC9diter" }),
|
|
693
739
|
/* @__PURE__ */ jsx("button", { className: `${s.actionIcon} ${s.danger}`, title: t("actions.deleteMessage"), "aria-label": t("actions.deleteMessage"), onClick: () => handleDeleteMessage(msg.id), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: t("actions.deleteMessage") }),
|
|
694
740
|
features.splitTicket && !msg.isInternal && /* @__PURE__ */ jsx("button", { className: s.actionIcon, title: t("actions.extractMessage"), "aria-label": t("actions.extractToNewTicket"), onClick: () => {
|
|
695
741
|
setSplitModal({ messageId: msg.id, preview: msg.body.slice(0, 200) });
|
package/package.json
CHANGED
|
@@ -129,6 +129,49 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
|
|
|
129
129
|
if (fileInputRef.current) fileInputRef.current.value = ''
|
|
130
130
|
}, [onFileUpload, emitChange])
|
|
131
131
|
|
|
132
|
+
const handleClearFormat = useCallback(() => {
|
|
133
|
+
document.execCommand('removeFormat')
|
|
134
|
+
document.execCommand('formatBlock', false, 'p')
|
|
135
|
+
editorRef.current?.focus()
|
|
136
|
+
setTimeout(emitChange, 0)
|
|
137
|
+
}, [emitChange])
|
|
138
|
+
|
|
139
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
140
|
+
if (e.key !== 'Enter' || e.shiftKey) return
|
|
141
|
+
const sel = window.getSelection()
|
|
142
|
+
if (!sel || sel.rangeCount === 0) return
|
|
143
|
+
// Find closest blockquote ancestor of the caret
|
|
144
|
+
let node: Node | null = sel.anchorNode
|
|
145
|
+
let blockquote: HTMLElement | null = null
|
|
146
|
+
while (node && node !== editorRef.current) {
|
|
147
|
+
if ((node as HTMLElement).nodeName === 'BLOCKQUOTE') {
|
|
148
|
+
blockquote = node as HTMLElement
|
|
149
|
+
break
|
|
150
|
+
}
|
|
151
|
+
node = node.parentNode
|
|
152
|
+
}
|
|
153
|
+
if (!blockquote) return
|
|
154
|
+
// Exit on Enter when the last line of the blockquote is empty
|
|
155
|
+
const text = blockquote.innerText || ''
|
|
156
|
+
if (text.replace(/\n+$/, '').length === 0 || text.endsWith('\n\n') || text.endsWith('\n')) {
|
|
157
|
+
e.preventDefault()
|
|
158
|
+
// Insert a paragraph after the blockquote and move caret into it
|
|
159
|
+
const p = document.createElement('p')
|
|
160
|
+
p.innerHTML = '<br>'
|
|
161
|
+
blockquote.after(p)
|
|
162
|
+
// Clean trailing empty <br> inside blockquote
|
|
163
|
+
if (blockquote.lastChild && (blockquote.lastChild as HTMLElement).nodeName === 'BR') {
|
|
164
|
+
blockquote.removeChild(blockquote.lastChild)
|
|
165
|
+
}
|
|
166
|
+
const range = document.createRange()
|
|
167
|
+
range.setStart(p, 0)
|
|
168
|
+
range.collapse(true)
|
|
169
|
+
sel.removeAllRanges()
|
|
170
|
+
sel.addRange(range)
|
|
171
|
+
setTimeout(emitChange, 0)
|
|
172
|
+
}
|
|
173
|
+
}, [emitChange])
|
|
174
|
+
|
|
132
175
|
const handlePaste = useCallback(async (e: React.ClipboardEvent) => {
|
|
133
176
|
if (!onFileUpload) return
|
|
134
177
|
const items = e.clipboardData?.items
|
|
@@ -215,6 +258,10 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
|
|
|
215
258
|
<button type="button" style={btn} onMouseDown={(e) => { e.preventDefault(); handleImageClick() }} title="Insérer une image">
|
|
216
259
|
🖼️
|
|
217
260
|
</button>
|
|
261
|
+
<span style={sep} />
|
|
262
|
+
<button type="button" style={{ ...btn, fontSize: '13px' }} onMouseDown={(e) => { e.preventDefault(); handleClearFormat() }} title="Effacer la mise en forme">
|
|
263
|
+
T<span style={{ fontSize: '10px', verticalAlign: 'super' }}>×</span>
|
|
264
|
+
</button>
|
|
218
265
|
</div>
|
|
219
266
|
|
|
220
267
|
{/* Editor area */}
|
|
@@ -233,6 +280,7 @@ export const RichTextEditor = forwardRef<RichTextEditorHandle, Props>(function R
|
|
|
233
280
|
onFocus={() => setFocused(true)}
|
|
234
281
|
onBlur={() => { setFocused(false); emitChange() }}
|
|
235
282
|
onPaste={handlePaste}
|
|
283
|
+
onKeyDown={handleKeyDown}
|
|
236
284
|
style={{
|
|
237
285
|
minHeight: `${minHeight}px`,
|
|
238
286
|
padding: '14px 16px',
|
|
@@ -133,6 +133,10 @@ export const TicketDetailClient: React.FC = () => {
|
|
|
133
133
|
const [isInternal, setIsInternal] = useState(false)
|
|
134
134
|
const [notifyClient, setNotifyClient] = useState(true)
|
|
135
135
|
const [sendAsClient, setSendAsClient] = useState(false)
|
|
136
|
+
// Inline message edit
|
|
137
|
+
const [editingMsgId, setEditingMsgId] = useState<string | number | null>(null)
|
|
138
|
+
const [editingBody, setEditingBody] = useState('')
|
|
139
|
+
const [editSaving, setEditSaving] = useState(false)
|
|
136
140
|
const [sending, setSending] = useState(false)
|
|
137
141
|
|
|
138
142
|
const [showMenu, setShowMenu] = useState(false)
|
|
@@ -441,6 +445,37 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
441
445
|
}
|
|
442
446
|
}
|
|
443
447
|
|
|
448
|
+
// Inline edit message (body only, clears bodyHtml so the edit shows as plain text)
|
|
449
|
+
const startEditMessage = (msg: Message) => {
|
|
450
|
+
setEditingMsgId(msg.id)
|
|
451
|
+
setEditingBody(msg.body || '')
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const cancelEditMessage = () => {
|
|
455
|
+
setEditingMsgId(null)
|
|
456
|
+
setEditingBody('')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const saveEditMessage = async () => {
|
|
460
|
+
if (editingMsgId === null) return
|
|
461
|
+
setEditSaving(true)
|
|
462
|
+
try {
|
|
463
|
+
const res = await fetch(`/api/ticket-messages/${editingMsgId}`, {
|
|
464
|
+
method: 'PATCH',
|
|
465
|
+
headers: { 'Content-Type': 'application/json' },
|
|
466
|
+
credentials: 'include',
|
|
467
|
+
body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true }),
|
|
468
|
+
})
|
|
469
|
+
if (res.ok) {
|
|
470
|
+
setEditingMsgId(null)
|
|
471
|
+
setEditingBody('')
|
|
472
|
+
fetchAll()
|
|
473
|
+
}
|
|
474
|
+
} catch { /* silent */ } finally {
|
|
475
|
+
setEditSaving(false)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
444
479
|
// #2 — Split ticket with modal
|
|
445
480
|
const handleSplitConfirm = async () => {
|
|
446
481
|
if (!splitModal || !splitSubject.trim()) return
|
|
@@ -617,7 +652,20 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
617
652
|
})()}
|
|
618
653
|
</span>
|
|
619
654
|
</div>
|
|
620
|
-
{
|
|
655
|
+
{editingMsgId === msg.id ? (
|
|
656
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
657
|
+
<textarea
|
|
658
|
+
value={editingBody}
|
|
659
|
+
onChange={(e) => setEditingBody(e.target.value)}
|
|
660
|
+
style={{ width: '100%', minHeight: 120, padding: 10, fontSize: 13, lineHeight: 1.5, fontFamily: 'inherit', border: '1px solid var(--theme-elevation-200)', borderRadius: 6, background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' }}
|
|
661
|
+
autoFocus
|
|
662
|
+
/>
|
|
663
|
+
<div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end' }}>
|
|
664
|
+
<button onClick={cancelEditMessage} disabled={editSaving} style={{ padding: '5px 12px', fontSize: 12, borderRadius: 5, border: '1px solid var(--theme-elevation-200)', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)', cursor: 'pointer' }}>Annuler</button>
|
|
665
|
+
<button onClick={saveEditMessage} disabled={editSaving || !editingBody.trim()} style={{ padding: '5px 12px', fontSize: 12, fontWeight: 600, borderRadius: 5, border: 'none', background: '#2563eb', color: '#fff', cursor: editSaving ? 'wait' : 'pointer', opacity: editSaving ? 0.6 : 1 }}>{editSaving ? 'Sauvegarde…' : 'Enregistrer'}</button>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
) : (msg as unknown as { deletedAt?: string }).deletedAt ? (
|
|
621
669
|
<div className={s.messageBody} style={{ color: '#94a3b8', fontStyle: 'italic' }}>{t('detail.messageDeleted')}</div>
|
|
622
670
|
) : msg.bodyHtml && hasCodeBlocks(msg.bodyHtml.replace(/<[^>]+>/g, '')) ? (
|
|
623
671
|
<CodeBlockRendererHtml html={msg.bodyHtml} />
|
|
@@ -641,13 +689,16 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
641
689
|
)}
|
|
642
690
|
</div>
|
|
643
691
|
{/* Hover actions — icon buttons with aria-labels (#7) */}
|
|
644
|
-
|
|
645
|
-
{
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
692
|
+
{editingMsgId !== msg.id && !(msg as unknown as { deletedAt?: string }).deletedAt && (
|
|
693
|
+
<div className={s.messageActions}>
|
|
694
|
+
<button className={s.actionIcon} title="Éditer" aria-label="Éditer le message" onClick={() => startEditMessage(msg)} style={{ fontSize: 11, width: 'auto', padding: '4px 8px' }}>Éditer</button>
|
|
695
|
+
{/* #2 — Undo toast instead of confirm() */}
|
|
696
|
+
<button className={`${s.actionIcon} ${s.danger}`} title={t('actions.deleteMessage')} aria-label={t('actions.deleteMessage')} onClick={() => handleDeleteMessage(msg.id)} style={{ fontSize: 11, width: 'auto', padding: '4px 8px' }}>{t('actions.deleteMessage')}</button>
|
|
697
|
+
{/* #2 — Split modal instead of prompt() */}
|
|
698
|
+
{features.splitTicket && !msg.isInternal && <button className={s.actionIcon} title={t('actions.extractMessage')} aria-label={t('actions.extractToNewTicket')} onClick={() => { setSplitModal({ messageId: msg.id, preview: msg.body.slice(0, 200) }); setSplitSubject(`Split: ${ticket.subject}`) }} style={{ fontSize: 11, width: 'auto', padding: '4px 8px' }}>{t('actions.extractMessage')}</button>}
|
|
699
|
+
{isAdmin && !msg.isInternal && <button className={s.actionIcon} title={t('actions.resendEmail')} aria-label={t('actions.resendEmail')} onClick={() => fetch('/api/support/resend-notification', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ messageId: msg.id }) })} style={{ fontSize: 11, width: 'auto', padding: '4px 8px' }}>{t('actions.resendEmail')}</button>}
|
|
700
|
+
</div>
|
|
701
|
+
)}
|
|
651
702
|
</div>
|
|
652
703
|
</React.Fragment>
|
|
653
704
|
)
|