fix(admin): resolve Tiptap editor bugs, dynamic RTL/LTR layout, sticky TOC, and equalize Bidi typography

This commit is contained in:
fatemeh sane
2026-09-17 10:10:50 +03:30
parent 049291c109
commit 7a0defc3d4
19 changed files with 685 additions and 135 deletions
+11 -1
View File
@@ -15,6 +15,7 @@ import type { CategoryOption } from '../components/admin/LuxeSelect'
import { ARTICLE_CATEGORIES } from './entities'
import { FEATURED_IMAGE_ALT_SUFFIX } from '../data/constants'
import { formatReadingTime } from '../utils/readingTime'
import { toArticleBodyDoc } from '../utils/tiptapDoc'
import type { Article, ArticleBodyDoc } from '../data/site'
const EMPTY_DOC = { type: 'doc', content: [{ type: 'paragraph' }] } as unknown as ArticleBodyDoc
@@ -111,6 +112,11 @@ export function ArticleWritePage() {
const { field: imageTitleField } = useController({ name: 'image_title', control })
const [createdId, setCreatedId] = useState<number | null>(null)
/**
* شناسه محتوای بارگذاری‌شده — با رسیدن مقاله از سرور عوض می‌شود تا
* بدنه ویرایشگر TipTap با متن واقعی مقاله مقداردهی شود.
*/
const [loadedContentKey, setLoadedContentKey] = useState<string | number>('new')
const [deleting, setDeleting] = useState(false)
const draftRef = useRef(draft)
draftRef.current = draft
@@ -144,9 +150,12 @@ export function ArticleWritePage() {
setDraft({
...article,
tags: normalizeTags(article.tags),
body: (Array.isArray(article.body) ? EMPTY_DOC : article.body) as ArticleBodyDoc,
// بدنه ذخیره‌شده (سند جدید یا پاراگراف‌های قدیمی) به سند قابل ویرایش تبدیل می‌شود
body: toArticleBodyDoc(article.body),
})
setCreatedId(article.id)
// بدنه ویرایشگر با محتوای بارگذاری‌شده همگام می‌شود
setLoadedContentKey(article.id)
// فرم متادیتا هم با مقادیر مقاله بارگذاری‌شده همگام می‌شود
reset({
title: article.title ?? '',
@@ -384,6 +393,7 @@ export function ArticleWritePage() {
{/* متن */}
<div className="mt-8">
<ArticleEditor
contentKey={loadedContentKey}
doc={draft.body as unknown as Record<string, unknown>}
onChange={(json) => {
// زمان مطالعه بهصورت پویا از متن سند محاسبه میشود (۲۰۰ واژه در دقیقه)
+4 -4
View File
@@ -5,6 +5,7 @@ import Swal from 'sweetalert2'
import { useSiteData } from '../api'
import { useAdmin, useFieldSaver } from './AdminContext'
import { adminApi } from './adminClient'
import { toArticleBodyDoc } from '../utils/tiptapDoc'
import type { EntityConfig, FieldConfig } from './entities'
import { ImageField } from '../components/admin/ImageField'
import { JalaliDatePicker } from '../components/admin/JalaliDatePicker'
@@ -413,10 +414,9 @@ function WysiwygField({
onSave: (value: unknown) => Promise<boolean>
registerFlush?: (fn: () => void) => void
}) {
const [doc, setDoc] = useState<Record<string, unknown>>(() =>
value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: { type: 'doc', content: [{ type: 'paragraph' }] },
const [doc, setDoc] = useState<Record<string, unknown>>(
// بدنه ذخیره‌شده (سند جدید، رشته JSON یا پاراگراف‌های قدیمی) به سند قابل ویرایش تبدیل می‌شود
() => toArticleBodyDoc(value),
)
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
const pending = useRef<Record<string, unknown> | null>(null)