fix: FAQ answer editor — stable keys, safe setContent, drop handler

- Use emitUpdate: false instead of fragile suppressUpdate ref
- Stable React keys for FAQ items — survive reorder/delete without editor desync
- Fix onDrop to check file type before preventDefault (non-media drops were swallowed)
This commit is contained in:
Fringg
2026-04-24 15:19:20 +03:00
parent 0adbfa50eb
commit a6850c8cbc

View File

@@ -259,7 +259,9 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
const isUploading = uploadCount > 0; const isUploading = uploadCount > 0;
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const activeUploadsRef = useRef(new Set<AbortController>()); const activeUploadsRef = useRef(new Set<AbortController>());
const suppressUpdate = useRef(false);
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const editor = useEditor({ const editor = useEditor({
extensions: FAQ_EDITOR_EXTENSIONS, extensions: FAQ_EDITOR_EXTENSIONS,
@@ -270,10 +272,9 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
}, },
}, },
onUpdate: ({ editor: e }) => { onUpdate: ({ editor: e }) => {
if (suppressUpdate.current) return;
const html = e.getHTML(); const html = e.getHTML();
const isEmpty = html === '<p></p>' || html === ''; const isEmpty = html === '<p></p>' || html === '';
onChange(isEmpty ? '' : html); onChangeRef.current(isEmpty ? '' : html);
}, },
}); });
@@ -283,9 +284,7 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
const currentHtml = editor.getHTML(); const currentHtml = editor.getHTML();
const normalizedCurrent = currentHtml === '<p></p>' ? '' : currentHtml; const normalizedCurrent = currentHtml === '<p></p>' ? '' : currentHtml;
if (normalizedCurrent !== value) { if (normalizedCurrent !== value) {
suppressUpdate.current = true; editor.commands.setContent(value || '', { emitUpdate: false });
editor.commands.setContent(value || '');
suppressUpdate.current = false;
} }
}, [value, editor]); }, [value, editor]);
@@ -499,9 +498,11 @@ function FaqAnswerEditor({ value, onChange }: { value: string; onChange: (html:
onDrop={(e) => { onDrop={(e) => {
setIsDragging(false); setIsDragging(false);
if (e.defaultPrevented) return; if (e.defaultPrevented) return;
e.preventDefault();
const file = e.dataTransfer.files[0]; const file = e.dataTransfer.files[0];
if (file) handleMediaUpload(file); if (file && (file.type.startsWith('image/') || file.type.startsWith('video/'))) {
e.preventDefault();
handleMediaUpload(file);
}
}} }}
> >
<EditorContent editor={editor} /> <EditorContent editor={editor} />
@@ -535,6 +536,25 @@ interface FaqBuilderProps {
function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) { function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) {
const { t } = useTranslation(); const { t } = useTranslation();
// Stable keys for React — travel with items through reorder/delete
const keyCounter = useRef(0);
const [itemKeys, setItemKeys] = useState<number[]>(() => items.map(() => keyCounter.current++));
// Sync key count when items change externally (e.g., locale switch resets items)
useEffect(() => {
setItemKeys((prev) => {
if (prev.length === items.length) return prev;
if (items.length > prev.length) {
const newKeys = [...prev];
for (let i = prev.length; i < items.length; i++) {
newKeys.push(keyCounter.current++);
}
return newKeys;
}
return prev.slice(0, items.length);
});
}, [items.length]);
const handleQuestionChange = useCallback( const handleQuestionChange = useCallback(
(index: number, value: string) => { (index: number, value: string) => {
const updated = items.map((item, i) => (i === index ? { ...item, q: value } : item)); const updated = items.map((item, i) => (i === index ? { ...item, q: value } : item));
@@ -553,12 +573,14 @@ function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) {
const handleRemove = useCallback( const handleRemove = useCallback(
(index: number) => { (index: number) => {
setItemKeys((prev) => prev.filter((_, i) => i !== index));
onChange(items.filter((_, i) => i !== index)); onChange(items.filter((_, i) => i !== index));
}, },
[items, onChange], [items, onChange],
); );
const handleAdd = useCallback(() => { const handleAdd = useCallback(() => {
setItemKeys((prev) => [...prev, keyCounter.current++]);
onChange([...items, { q: '', a: '' }]); onChange([...items, { q: '', a: '' }]);
}, [items, onChange]); }, [items, onChange]);
@@ -567,6 +589,11 @@ function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) {
if (index === 0) return; if (index === 0) return;
const updated = [...items]; const updated = [...items];
[updated[index - 1], updated[index]] = [updated[index], updated[index - 1]]; [updated[index - 1], updated[index]] = [updated[index], updated[index - 1]];
setItemKeys((prev) => {
const k = [...prev];
[k[index - 1], k[index]] = [k[index], k[index - 1]];
return k;
});
onChange(updated); onChange(updated);
}, },
[items, onChange], [items, onChange],
@@ -577,6 +604,11 @@ function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) {
if (index >= items.length - 1) return; if (index >= items.length - 1) return;
const updated = [...items]; const updated = [...items];
[updated[index], updated[index + 1]] = [updated[index + 1], updated[index]]; [updated[index], updated[index + 1]] = [updated[index + 1], updated[index]];
setItemKeys((prev) => {
const k = [...prev];
[k[index], k[index + 1]] = [k[index + 1], k[index]];
return k;
});
onChange(updated); onChange(updated);
}, },
[items, onChange], [items, onChange],
@@ -603,7 +635,7 @@ function FaqBuilder({ items, onChange, locale, localeLabel }: FaqBuilderProps) {
{items.map((item, index) => ( {items.map((item, index) => (
<div <div
key={index} key={itemKeys[index] ?? index}
className="rounded-xl border border-dark-700 bg-dark-800/50 p-4 transition-all hover:border-dark-600" className="rounded-xl border border-dark-700 bg-dark-800/50 p-4 transition-all hover:border-dark-600"
> >
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">