@consilioweb/payload-support 0.9.5 → 0.9.7
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/index.cjs
CHANGED
|
@@ -1457,15 +1457,14 @@ function emailRichContent(html, config) {
|
|
|
1457
1457
|
function convertFencedCodeBlocks(input) {
|
|
1458
1458
|
if (!input.includes("```")) return input;
|
|
1459
1459
|
const normalized = input.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div)>\s*<(p|div)[^>]*>/gi, "\n").replace(/<(p|div)[^>]*>/gi, "").replace(/<\/(p|div)>/gi, "\n");
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
return
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
if (chunk.startsWith('<div style="margin: 16px 0')) return chunk;
|
|
1460
|
+
const codeBlocks = [];
|
|
1461
|
+
const withMarkers = normalized.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, lang, code) => {
|
|
1462
|
+
codeBlocks.push(renderCodeBlockHtml(lang || "", code || ""));
|
|
1463
|
+
return `\0CODEBLOCK_${codeBlocks.length - 1}\0`;
|
|
1464
|
+
});
|
|
1465
|
+
return withMarkers.split(/(\x00CODEBLOCK_\d+\x00)/g).map((chunk) => {
|
|
1466
|
+
const markerMatch = chunk.match(/^\x00CODEBLOCK_(\d+)\x00$/);
|
|
1467
|
+
if (markerMatch) return codeBlocks[parseInt(markerMatch[1], 10)];
|
|
1469
1468
|
return chunk.split("\n").map((line) => line.trim() ? `<p>${line}</p>` : "").join("");
|
|
1470
1469
|
}).join("");
|
|
1471
1470
|
}
|
|
@@ -1500,9 +1499,40 @@ function emailButton(text, url, color = "primary", config) {
|
|
|
1500
1499
|
</div>
|
|
1501
1500
|
`;
|
|
1502
1501
|
}
|
|
1502
|
+
function renderCodeBlockHtml(lang, code) {
|
|
1503
|
+
const cleanCode = escapeHtml(code.replace(/\n$/, ""));
|
|
1504
|
+
const label = lang ? lang.trim() : "code";
|
|
1505
|
+
return `<div style="margin: 12px 0; border: 1px solid #1f2937; border-radius: 8px; overflow: hidden; background: #0f172a;">
|
|
1506
|
+
<div style="padding: 6px 14px; background: #1e293b; border-bottom: 1px solid #334155; font-family: 'SF Mono', 'Fira Code', Consolas, monospace; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em;">${escapeHtml(label)}</div>
|
|
1507
|
+
<pre style="margin: 0; padding: 14px 16px; overflow-x: auto; background: #0f172a; color: #e2e8f0; font-family: 'SF Mono', 'Fira Code', Consolas, monospace; font-size: 13px; line-height: 1.6; white-space: pre;"><code style="background: transparent; padding: 0; color: inherit; font-family: inherit; font-size: inherit;">${cleanCode}</code></pre>
|
|
1508
|
+
</div>`;
|
|
1509
|
+
}
|
|
1503
1510
|
function emailQuote(content, borderColor, config) {
|
|
1504
1511
|
const c = resolveConfig(config);
|
|
1505
1512
|
const color = borderColor || c.brandColor;
|
|
1513
|
+
if (content && content.includes("```")) {
|
|
1514
|
+
const parts = [];
|
|
1515
|
+
const regex = /```(\w*)\n?([\s\S]*?)```/g;
|
|
1516
|
+
let lastIndex = 0;
|
|
1517
|
+
let match;
|
|
1518
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1519
|
+
const textBefore = content.slice(lastIndex, match.index).trim();
|
|
1520
|
+
if (textBefore) {
|
|
1521
|
+
parts.push(`<p style="margin: 0 0 12px 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(textBefore)}</p>`);
|
|
1522
|
+
}
|
|
1523
|
+
parts.push(renderCodeBlockHtml(match[1] || "", match[2] || ""));
|
|
1524
|
+
lastIndex = match.index + match[0].length;
|
|
1525
|
+
}
|
|
1526
|
+
const textAfter = content.slice(lastIndex).trim();
|
|
1527
|
+
if (textAfter) {
|
|
1528
|
+
parts.push(`<p style="margin: 12px 0 0 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(textAfter)}</p>`);
|
|
1529
|
+
}
|
|
1530
|
+
return `
|
|
1531
|
+
<div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
|
|
1532
|
+
${parts.join("")}
|
|
1533
|
+
</div>
|
|
1534
|
+
`;
|
|
1535
|
+
}
|
|
1506
1536
|
return `
|
|
1507
1537
|
<div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
|
|
1508
1538
|
<p style="margin: 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(content)}</p>
|
package/dist/index.js
CHANGED
|
@@ -1451,15 +1451,14 @@ function emailRichContent(html, config) {
|
|
|
1451
1451
|
function convertFencedCodeBlocks(input) {
|
|
1452
1452
|
if (!input.includes("```")) return input;
|
|
1453
1453
|
const normalized = input.replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div)>\s*<(p|div)[^>]*>/gi, "\n").replace(/<(p|div)[^>]*>/gi, "").replace(/<\/(p|div)>/gi, "\n");
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
return
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
if (chunk.startsWith('<div style="margin: 16px 0')) return chunk;
|
|
1454
|
+
const codeBlocks = [];
|
|
1455
|
+
const withMarkers = normalized.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, lang, code) => {
|
|
1456
|
+
codeBlocks.push(renderCodeBlockHtml(lang || "", code || ""));
|
|
1457
|
+
return `\0CODEBLOCK_${codeBlocks.length - 1}\0`;
|
|
1458
|
+
});
|
|
1459
|
+
return withMarkers.split(/(\x00CODEBLOCK_\d+\x00)/g).map((chunk) => {
|
|
1460
|
+
const markerMatch = chunk.match(/^\x00CODEBLOCK_(\d+)\x00$/);
|
|
1461
|
+
if (markerMatch) return codeBlocks[parseInt(markerMatch[1], 10)];
|
|
1463
1462
|
return chunk.split("\n").map((line) => line.trim() ? `<p>${line}</p>` : "").join("");
|
|
1464
1463
|
}).join("");
|
|
1465
1464
|
}
|
|
@@ -1494,9 +1493,40 @@ function emailButton(text, url, color = "primary", config) {
|
|
|
1494
1493
|
</div>
|
|
1495
1494
|
`;
|
|
1496
1495
|
}
|
|
1496
|
+
function renderCodeBlockHtml(lang, code) {
|
|
1497
|
+
const cleanCode = escapeHtml(code.replace(/\n$/, ""));
|
|
1498
|
+
const label = lang ? lang.trim() : "code";
|
|
1499
|
+
return `<div style="margin: 12px 0; border: 1px solid #1f2937; border-radius: 8px; overflow: hidden; background: #0f172a;">
|
|
1500
|
+
<div style="padding: 6px 14px; background: #1e293b; border-bottom: 1px solid #334155; font-family: 'SF Mono', 'Fira Code', Consolas, monospace; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em;">${escapeHtml(label)}</div>
|
|
1501
|
+
<pre style="margin: 0; padding: 14px 16px; overflow-x: auto; background: #0f172a; color: #e2e8f0; font-family: 'SF Mono', 'Fira Code', Consolas, monospace; font-size: 13px; line-height: 1.6; white-space: pre;"><code style="background: transparent; padding: 0; color: inherit; font-family: inherit; font-size: inherit;">${cleanCode}</code></pre>
|
|
1502
|
+
</div>`;
|
|
1503
|
+
}
|
|
1497
1504
|
function emailQuote(content, borderColor, config) {
|
|
1498
1505
|
const c = resolveConfig(config);
|
|
1499
1506
|
const color = borderColor || c.brandColor;
|
|
1507
|
+
if (content && content.includes("```")) {
|
|
1508
|
+
const parts = [];
|
|
1509
|
+
const regex = /```(\w*)\n?([\s\S]*?)```/g;
|
|
1510
|
+
let lastIndex = 0;
|
|
1511
|
+
let match;
|
|
1512
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1513
|
+
const textBefore = content.slice(lastIndex, match.index).trim();
|
|
1514
|
+
if (textBefore) {
|
|
1515
|
+
parts.push(`<p style="margin: 0 0 12px 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(textBefore)}</p>`);
|
|
1516
|
+
}
|
|
1517
|
+
parts.push(renderCodeBlockHtml(match[1] || "", match[2] || ""));
|
|
1518
|
+
lastIndex = match.index + match[0].length;
|
|
1519
|
+
}
|
|
1520
|
+
const textAfter = content.slice(lastIndex).trim();
|
|
1521
|
+
if (textAfter) {
|
|
1522
|
+
parts.push(`<p style="margin: 12px 0 0 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(textAfter)}</p>`);
|
|
1523
|
+
}
|
|
1524
|
+
return `
|
|
1525
|
+
<div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
|
|
1526
|
+
${parts.join("")}
|
|
1527
|
+
</div>
|
|
1528
|
+
`;
|
|
1529
|
+
}
|
|
1500
1530
|
return `
|
|
1501
1531
|
<div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
|
|
1502
1532
|
<p style="margin: 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(content)}</p>
|
|
@@ -138,6 +138,7 @@ const TicketDetailClient = () => {
|
|
|
138
138
|
const [replyHtml, setReplyHtml] = React.useState("");
|
|
139
139
|
const [isInternal, setIsInternal] = React.useState(false);
|
|
140
140
|
const [notifyClient, setNotifyClient] = React.useState(true);
|
|
141
|
+
const [sendAsClient, setSendAsClient] = React.useState(false);
|
|
141
142
|
const [sending, setSending] = React.useState(false);
|
|
142
143
|
const [showMenu, setShowMenu] = React.useState(false);
|
|
143
144
|
const [clientTyping, setClientTyping] = React.useState(false);
|
|
@@ -419,12 +420,21 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
419
420
|
method: "POST",
|
|
420
421
|
headers: { "Content-Type": "application/json" },
|
|
421
422
|
credentials: "include",
|
|
422
|
-
body: JSON.stringify({
|
|
423
|
+
body: JSON.stringify({
|
|
424
|
+
ticket: Number(ticketId),
|
|
425
|
+
body: finalBody,
|
|
426
|
+
...finalHtml ? { bodyHtml: finalHtml } : {},
|
|
427
|
+
authorType: sendAsClient ? "client" : "admin",
|
|
428
|
+
...sendAsClient && client ? { authorClient: client.id } : {},
|
|
429
|
+
isInternal: sendAsClient ? false : isInternal,
|
|
430
|
+
skipNotification: sendAsClient || isInternal || !notifyClient
|
|
431
|
+
})
|
|
423
432
|
});
|
|
424
433
|
if (res.ok) {
|
|
425
434
|
setReplyBody("");
|
|
426
435
|
setReplyHtml("");
|
|
427
436
|
setIsInternal(false);
|
|
437
|
+
setSendAsClient(false);
|
|
428
438
|
setPendingFiles([]);
|
|
429
439
|
editorRef.current?.clear();
|
|
430
440
|
fetchAll();
|
|
@@ -789,18 +799,39 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
789
799
|
] }, i)) }),
|
|
790
800
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.composerFooter, children: [
|
|
791
801
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.composerOptions, children: [
|
|
792
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
802
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
803
|
+
"select",
|
|
804
|
+
{
|
|
805
|
+
value: sendAsClient ? "client" : "admin",
|
|
806
|
+
onChange: (e) => {
|
|
807
|
+
const asClient = e.target.value === "client";
|
|
808
|
+
setSendAsClient(asClient);
|
|
809
|
+
if (asClient) {
|
|
810
|
+
setIsInternal(false);
|
|
811
|
+
setNotifyClient(false);
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
style: { fontSize: "12px", padding: "4px 8px", fontWeight: 600, borderRadius: 6, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
|
|
815
|
+
children: [
|
|
816
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "admin", children: "En tant que : Support" }),
|
|
817
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "client", children: "En tant que : Client" })
|
|
818
|
+
]
|
|
819
|
+
}
|
|
820
|
+
),
|
|
821
|
+
!sendAsClient && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
822
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
823
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
|
|
824
|
+
" ",
|
|
825
|
+
t("detail.internalNote")
|
|
826
|
+
] }),
|
|
827
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
828
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: notifyClient, onChange: (e) => setNotifyClient(e.target.checked), disabled: isInternal }),
|
|
829
|
+
" ",
|
|
830
|
+
t("detail.notify")
|
|
831
|
+
] })
|
|
801
832
|
] })
|
|
802
833
|
] }),
|
|
803
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: `${s__default.default.sendBtn} ${isInternal ? s__default.default.sendBtnInternal : ""}`, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
|
|
834
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: `${s__default.default.sendBtn} ${isInternal ? s__default.default.sendBtnInternal : ""}`, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : sendAsClient ? "Ajouter message client" : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
|
|
804
835
|
] })
|
|
805
836
|
]
|
|
806
837
|
}
|
|
@@ -131,6 +131,7 @@ const TicketDetailClient = () => {
|
|
|
131
131
|
const [replyHtml, setReplyHtml] = useState("");
|
|
132
132
|
const [isInternal, setIsInternal] = useState(false);
|
|
133
133
|
const [notifyClient, setNotifyClient] = useState(true);
|
|
134
|
+
const [sendAsClient, setSendAsClient] = useState(false);
|
|
134
135
|
const [sending, setSending] = useState(false);
|
|
135
136
|
const [showMenu, setShowMenu] = useState(false);
|
|
136
137
|
const [clientTyping, setClientTyping] = useState(false);
|
|
@@ -412,12 +413,21 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
412
413
|
method: "POST",
|
|
413
414
|
headers: { "Content-Type": "application/json" },
|
|
414
415
|
credentials: "include",
|
|
415
|
-
body: JSON.stringify({
|
|
416
|
+
body: JSON.stringify({
|
|
417
|
+
ticket: Number(ticketId),
|
|
418
|
+
body: finalBody,
|
|
419
|
+
...finalHtml ? { bodyHtml: finalHtml } : {},
|
|
420
|
+
authorType: sendAsClient ? "client" : "admin",
|
|
421
|
+
...sendAsClient && client ? { authorClient: client.id } : {},
|
|
422
|
+
isInternal: sendAsClient ? false : isInternal,
|
|
423
|
+
skipNotification: sendAsClient || isInternal || !notifyClient
|
|
424
|
+
})
|
|
416
425
|
});
|
|
417
426
|
if (res.ok) {
|
|
418
427
|
setReplyBody("");
|
|
419
428
|
setReplyHtml("");
|
|
420
429
|
setIsInternal(false);
|
|
430
|
+
setSendAsClient(false);
|
|
421
431
|
setPendingFiles([]);
|
|
422
432
|
editorRef.current?.clear();
|
|
423
433
|
fetchAll();
|
|
@@ -782,18 +792,39 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
782
792
|
] }, i)) }),
|
|
783
793
|
/* @__PURE__ */ jsxs("div", { className: s.composerFooter, children: [
|
|
784
794
|
/* @__PURE__ */ jsxs("div", { className: s.composerOptions, children: [
|
|
785
|
-
/* @__PURE__ */ jsxs(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
795
|
+
/* @__PURE__ */ jsxs(
|
|
796
|
+
"select",
|
|
797
|
+
{
|
|
798
|
+
value: sendAsClient ? "client" : "admin",
|
|
799
|
+
onChange: (e) => {
|
|
800
|
+
const asClient = e.target.value === "client";
|
|
801
|
+
setSendAsClient(asClient);
|
|
802
|
+
if (asClient) {
|
|
803
|
+
setIsInternal(false);
|
|
804
|
+
setNotifyClient(false);
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
style: { fontSize: "12px", padding: "4px 8px", fontWeight: 600, borderRadius: 6, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
|
|
808
|
+
children: [
|
|
809
|
+
/* @__PURE__ */ jsx("option", { value: "admin", children: "En tant que : Support" }),
|
|
810
|
+
/* @__PURE__ */ jsx("option", { value: "client", children: "En tant que : Client" })
|
|
811
|
+
]
|
|
812
|
+
}
|
|
813
|
+
),
|
|
814
|
+
!sendAsClient && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
815
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
816
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
|
|
817
|
+
" ",
|
|
818
|
+
t("detail.internalNote")
|
|
819
|
+
] }),
|
|
820
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
821
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: notifyClient, onChange: (e) => setNotifyClient(e.target.checked), disabled: isInternal }),
|
|
822
|
+
" ",
|
|
823
|
+
t("detail.notify")
|
|
824
|
+
] })
|
|
794
825
|
] })
|
|
795
826
|
] }),
|
|
796
|
-
/* @__PURE__ */ jsx("button", { className: `${s.sendBtn} ${isInternal ? s.sendBtnInternal : ""}`, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
|
|
827
|
+
/* @__PURE__ */ jsx("button", { className: `${s.sendBtn} ${isInternal ? s.sendBtnInternal : ""}`, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : sendAsClient ? "Ajouter message client" : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
|
|
797
828
|
] })
|
|
798
829
|
]
|
|
799
830
|
}
|
package/package.json
CHANGED
|
@@ -88,19 +88,19 @@ export function emailRichContent(html: string, config?: EmailTemplateConfig): st
|
|
|
88
88
|
.replace(/<(p|div)[^>]*>/gi, '')
|
|
89
89
|
.replace(/<\/(p|div)>/gi, '\n')
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<pre style="margin: 0; padding: 14px 16px; overflow-x: auto; background: #0f172a; color: #e2e8f0; font-family: 'SF Mono', 'Fira Code', Consolas, monospace; font-size: 13px; line-height: 1.6; white-space: pre;"><code style="background: transparent; padding: 0; color: inherit; font-family: inherit; font-size: inherit;">${cleanCode}</code></pre>
|
|
97
|
-
</div>`
|
|
91
|
+
// Replace code blocks with markers to protect them from paragraph wrapping
|
|
92
|
+
const codeBlocks: string[] = []
|
|
93
|
+
const withMarkers = normalized.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, lang, code) => {
|
|
94
|
+
codeBlocks.push(renderCodeBlockHtml(lang || '', code || ''))
|
|
95
|
+
return `\x00CODEBLOCK_${codeBlocks.length - 1}\x00`
|
|
98
96
|
})
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
|
|
98
|
+
// Wrap non-marker text lines in <p>, then restore code blocks
|
|
99
|
+
return withMarkers
|
|
100
|
+
.split(/(\x00CODEBLOCK_\d+\x00)/g)
|
|
101
101
|
.map((chunk) => {
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
const markerMatch = chunk.match(/^\x00CODEBLOCK_(\d+)\x00$/)
|
|
103
|
+
if (markerMatch) return codeBlocks[parseInt(markerMatch[1], 10)]
|
|
104
104
|
return chunk
|
|
105
105
|
.split('\n')
|
|
106
106
|
.map((line) => line.trim() ? `<p>${line}</p>` : '')
|
|
@@ -165,11 +165,52 @@ export function emailButton(text: string, url: string, color: ButtonColor = 'pri
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
*
|
|
168
|
+
* Render a single fenced code block as styled email HTML.
|
|
169
|
+
* Email clients don't run JS so we just use a nice static layout.
|
|
170
|
+
*/
|
|
171
|
+
function renderCodeBlockHtml(lang: string, code: string): string {
|
|
172
|
+
const cleanCode = escapeHtml(code.replace(/\n$/, ''))
|
|
173
|
+
const label = lang ? lang.trim() : 'code'
|
|
174
|
+
return `<div style="margin: 12px 0; border: 1px solid #1f2937; border-radius: 8px; overflow: hidden; background: #0f172a;">
|
|
175
|
+
<div style="padding: 6px 14px; background: #1e293b; border-bottom: 1px solid #334155; font-family: 'SF Mono', 'Fira Code', Consolas, monospace; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em;">${escapeHtml(label)}</div>
|
|
176
|
+
<pre style="margin: 0; padding: 14px 16px; overflow-x: auto; background: #0f172a; color: #e2e8f0; font-family: 'SF Mono', 'Fira Code', Consolas, monospace; font-size: 13px; line-height: 1.6; white-space: pre;"><code style="background: transparent; padding: 0; color: inherit; font-family: inherit; font-size: inherit;">${cleanCode}</code></pre>
|
|
177
|
+
</div>`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Quote block for message previews — supports fenced code blocks.
|
|
169
182
|
*/
|
|
170
183
|
export function emailQuote(content: string, borderColor?: string, config?: EmailTemplateConfig): string {
|
|
171
184
|
const c = resolveConfig(config)
|
|
172
185
|
const color = borderColor || c.brandColor
|
|
186
|
+
|
|
187
|
+
// If content has fenced code blocks, render them as styled blocks
|
|
188
|
+
if (content && content.includes('```')) {
|
|
189
|
+
const parts: string[] = []
|
|
190
|
+
const regex = /```(\w*)\n?([\s\S]*?)```/g
|
|
191
|
+
let lastIndex = 0
|
|
192
|
+
let match: RegExpExecArray | null
|
|
193
|
+
while ((match = regex.exec(content)) !== null) {
|
|
194
|
+
// Text before the code block
|
|
195
|
+
const textBefore = content.slice(lastIndex, match.index).trim()
|
|
196
|
+
if (textBefore) {
|
|
197
|
+
parts.push(`<p style="margin: 0 0 12px 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(textBefore)}</p>`)
|
|
198
|
+
}
|
|
199
|
+
parts.push(renderCodeBlockHtml(match[1] || '', match[2] || ''))
|
|
200
|
+
lastIndex = match.index + match[0].length
|
|
201
|
+
}
|
|
202
|
+
// Remaining text after the last code block
|
|
203
|
+
const textAfter = content.slice(lastIndex).trim()
|
|
204
|
+
if (textAfter) {
|
|
205
|
+
parts.push(`<p style="margin: 12px 0 0 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(textAfter)}</p>`)
|
|
206
|
+
}
|
|
207
|
+
return `
|
|
208
|
+
<div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
|
|
209
|
+
${parts.join('')}
|
|
210
|
+
</div>
|
|
211
|
+
`
|
|
212
|
+
}
|
|
213
|
+
|
|
173
214
|
return `
|
|
174
215
|
<div style="margin: 24px 0; padding: 20px 24px; background: #f8f9fa; border-left: 4px solid ${color}; border-radius: 0 8px 8px 0;">
|
|
175
216
|
<p style="margin: 0; font-size: 15px; line-height: 1.75; color: #333333; white-space: pre-wrap;">${escapeHtml(content)}</p>
|
|
@@ -132,6 +132,7 @@ export const TicketDetailClient: React.FC = () => {
|
|
|
132
132
|
const [replyHtml, setReplyHtml] = useState('')
|
|
133
133
|
const [isInternal, setIsInternal] = useState(false)
|
|
134
134
|
const [notifyClient, setNotifyClient] = useState(true)
|
|
135
|
+
const [sendAsClient, setSendAsClient] = useState(false)
|
|
135
136
|
const [sending, setSending] = useState(false)
|
|
136
137
|
|
|
137
138
|
const [showMenu, setShowMenu] = useState(false)
|
|
@@ -396,8 +397,16 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
396
397
|
const finalHtml = uploadedLinks.length > 0 ? `${replyHtml || replyBody.trim()}<br/><br/>${uploadedLinks.map((l) => l.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')).join('<br/>')}` : (replyHtml || undefined)
|
|
397
398
|
|
|
398
399
|
const res = await fetch('/api/ticket-messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
|
|
399
|
-
body: JSON.stringify({
|
|
400
|
-
|
|
400
|
+
body: JSON.stringify({
|
|
401
|
+
ticket: Number(ticketId),
|
|
402
|
+
body: finalBody,
|
|
403
|
+
...(finalHtml ? { bodyHtml: finalHtml } : {}),
|
|
404
|
+
authorType: sendAsClient ? 'client' : 'admin',
|
|
405
|
+
...(sendAsClient && client ? { authorClient: client.id } : {}),
|
|
406
|
+
isInternal: sendAsClient ? false : isInternal,
|
|
407
|
+
skipNotification: sendAsClient || isInternal || !notifyClient,
|
|
408
|
+
}) })
|
|
409
|
+
if (res.ok) { setReplyBody(''); setReplyHtml(''); setIsInternal(false); setSendAsClient(false); setPendingFiles([]); editorRef.current?.clear(); fetchAll() }
|
|
401
410
|
} catch {} finally { setSending(false) }
|
|
402
411
|
}
|
|
403
412
|
|
|
@@ -718,13 +727,27 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
718
727
|
)}
|
|
719
728
|
<div className={s.composerFooter}>
|
|
720
729
|
<div className={s.composerOptions}>
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
730
|
+
<select
|
|
731
|
+
value={sendAsClient ? 'client' : 'admin'}
|
|
732
|
+
onChange={(e) => {
|
|
733
|
+
const asClient = e.target.value === 'client'
|
|
734
|
+
setSendAsClient(asClient)
|
|
735
|
+
if (asClient) { setIsInternal(false); setNotifyClient(false) }
|
|
736
|
+
}}
|
|
737
|
+
style={{ fontSize: '12px', padding: '4px 8px', fontWeight: 600, borderRadius: 6, border: '1px solid var(--theme-elevation-200)', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' }}
|
|
738
|
+
>
|
|
739
|
+
<option value="admin">En tant que : Support</option>
|
|
740
|
+
<option value="client">En tant que : Client</option>
|
|
741
|
+
</select>
|
|
742
|
+
{!sendAsClient && (
|
|
743
|
+
<>
|
|
744
|
+
<label><input type="checkbox" checked={isInternal} onChange={(e) => setIsInternal(e.target.checked)} /> {t('detail.internalNote')}</label>
|
|
745
|
+
<label><input type="checkbox" checked={notifyClient} onChange={(e) => setNotifyClient(e.target.checked)} disabled={isInternal} /> {t('detail.notify')}</label>
|
|
746
|
+
</>
|
|
747
|
+
)}
|
|
724
748
|
</div>
|
|
725
|
-
{/* #9 — "Send ->" -> "Envoyer ->" */}
|
|
726
749
|
<button className={`${s.sendBtn} ${isInternal ? s.sendBtnInternal : ''}`} onClick={handleSend} disabled={sending || !replyBody.trim()} data-action="send-reply">
|
|
727
|
-
{sending ? t('detail.sending') : isInternal ? t('detail.sendNote') : t('detail.sendReply')}
|
|
750
|
+
{sending ? t('detail.sending') : sendAsClient ? 'Ajouter message client' : isInternal ? t('detail.sendNote') : t('detail.sendReply')}
|
|
728
751
|
</button>
|
|
729
752
|
</div>
|
|
730
753
|
</div>
|