@consilioweb/payload-support 0.9.7 → 0.9.8

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.
@@ -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.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 }),
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.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 }),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-support",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -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
- {(msg as unknown as { deletedAt?: string }).deletedAt ? (
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
- <div className={s.messageActions}>
645
- {/* #2 — Undo toast instead of confirm() */}
646
- <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>
647
- {/* #2 — Split modal instead of prompt() */}
648
- {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>}
649
- {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>}
650
- </div>
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
  )