@consilioweb/payload-support 0.9.8 → 0.9.10
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 +26 -4
- package/dist/views/TicketDetailView/client.js +26 -4
- package/package.json +1 -1
- package/src/components/RichTextEditor/index.tsx +48 -0
- package/src/views/TicketDetailView/client.tsx +18 -1
|
@@ -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",
|
|
@@ -832,10 +832,32 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
832
832
|
cannedResponses.map((cr) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: String(cr.id), children: cr.title }, cr.id))
|
|
833
833
|
] })
|
|
834
834
|
] }),
|
|
835
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
835
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
836
|
+
index.RichTextEditor,
|
|
837
|
+
{
|
|
838
|
+
ref: editorRef,
|
|
839
|
+
onChange: (html, text) => {
|
|
840
|
+
setReplyHtml(html);
|
|
841
|
+
setReplyBody(text);
|
|
842
|
+
},
|
|
843
|
+
placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }),
|
|
844
|
+
minHeight: 100,
|
|
845
|
+
borderColor: "transparent",
|
|
846
|
+
onFileUpload: async (file) => {
|
|
847
|
+
try {
|
|
848
|
+
const formData = new FormData();
|
|
849
|
+
formData.append("file", file);
|
|
850
|
+
formData.append("_payload", JSON.stringify({ alt: file.name }));
|
|
851
|
+
const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
|
|
852
|
+
if (!ur.ok) return null;
|
|
853
|
+
const ud = await ur.json();
|
|
854
|
+
return ud.doc?.url || null;
|
|
855
|
+
} catch {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
),
|
|
839
861
|
pendingFiles.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.uploadPreview, children: pendingFiles.map((f, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.uploadPreviewItem, children: [
|
|
840
862
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
841
863
|
"PJ ",
|
|
@@ -825,10 +825,32 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
825
825
|
cannedResponses.map((cr) => /* @__PURE__ */ jsx("option", { value: String(cr.id), children: cr.title }, cr.id))
|
|
826
826
|
] })
|
|
827
827
|
] }),
|
|
828
|
-
/* @__PURE__ */ jsx(
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
828
|
+
/* @__PURE__ */ jsx(
|
|
829
|
+
RichTextEditor,
|
|
830
|
+
{
|
|
831
|
+
ref: editorRef,
|
|
832
|
+
onChange: (html, text) => {
|
|
833
|
+
setReplyHtml(html);
|
|
834
|
+
setReplyBody(text);
|
|
835
|
+
},
|
|
836
|
+
placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }),
|
|
837
|
+
minHeight: 100,
|
|
838
|
+
borderColor: "transparent",
|
|
839
|
+
onFileUpload: async (file) => {
|
|
840
|
+
try {
|
|
841
|
+
const formData = new FormData();
|
|
842
|
+
formData.append("file", file);
|
|
843
|
+
formData.append("_payload", JSON.stringify({ alt: file.name }));
|
|
844
|
+
const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
|
|
845
|
+
if (!ur.ok) return null;
|
|
846
|
+
const ud = await ur.json();
|
|
847
|
+
return ud.doc?.url || null;
|
|
848
|
+
} catch {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
),
|
|
832
854
|
pendingFiles.length > 0 && /* @__PURE__ */ jsx("div", { className: s.uploadPreview, children: pendingFiles.map((f, i) => /* @__PURE__ */ jsxs("div", { className: s.uploadPreviewItem, children: [
|
|
833
855
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
834
856
|
"PJ ",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consilioweb/payload-support",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
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",
|
|
@@ -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',
|
|
@@ -764,7 +764,24 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
764
764
|
</select>
|
|
765
765
|
)}
|
|
766
766
|
</div>
|
|
767
|
-
<RichTextEditor
|
|
767
|
+
<RichTextEditor
|
|
768
|
+
ref={editorRef}
|
|
769
|
+
onChange={(html, text) => { setReplyHtml(html); setReplyBody(text) }}
|
|
770
|
+
placeholder={isInternal ? t('composer.placeholderInternal') : t('composer.placeholderReplyTo', { name: client?.firstName || 'client' })}
|
|
771
|
+
minHeight={100}
|
|
772
|
+
borderColor="transparent"
|
|
773
|
+
onFileUpload={async (file) => {
|
|
774
|
+
try {
|
|
775
|
+
const formData = new FormData()
|
|
776
|
+
formData.append('file', file)
|
|
777
|
+
formData.append('_payload', JSON.stringify({ alt: file.name }))
|
|
778
|
+
const ur = await fetch('/api/media', { method: 'POST', credentials: 'include', body: formData })
|
|
779
|
+
if (!ur.ok) return null
|
|
780
|
+
const ud = await ur.json()
|
|
781
|
+
return ud.doc?.url || null
|
|
782
|
+
} catch { return null }
|
|
783
|
+
}}
|
|
784
|
+
/>
|
|
768
785
|
{/* #5 — File upload preview */}
|
|
769
786
|
{pendingFiles.length > 0 && (
|
|
770
787
|
<div className={s.uploadPreview}>
|