core/panel/modals/TopicEditModal.tsx

347 lines
12 KiB
TypeScript
Raw Normal View History

2025-07-02 19:30:21 +00:00
import { createEffect, createSignal, For, Show } from 'solid-js'
import { Topic, useData } from '../context/data'
import styles from '../styles/Form.module.css'
import modalStyles from '../styles/Modal.module.css'
import EditableCodePreview from '../ui/EditableCodePreview'
2025-06-30 18:25:26 +00:00
import Modal from '../ui/Modal'
interface TopicEditModalProps {
2025-07-02 19:30:21 +00:00
topic: Topic
2025-06-30 18:25:26 +00:00
isOpen: boolean
onClose: () => void
2025-07-02 19:30:21 +00:00
onSave: (updatedTopic: Topic) => void
onError?: (message: string) => void
2025-06-30 18:25:26 +00:00
}
2025-07-02 19:30:21 +00:00
export default function TopicEditModal(props: TopicEditModalProps) {
const { communities, topics, getCommunityName, selectedCommunity } = useData()
// Состояние формы
const [formData, setFormData] = createSignal({
2025-06-30 18:25:26 +00:00
id: 0,
title: '',
2025-07-02 19:30:21 +00:00
slug: '',
2025-06-30 18:25:26 +00:00
body: '',
community: 0,
2025-07-02 19:30:21 +00:00
parent_ids: [] as number[]
2025-06-30 18:25:26 +00:00
})
2025-07-02 19:30:21 +00:00
// Состояние для выбора родителей
const [availableParents, setAvailableParents] = createSignal<Topic[]>([])
const [parentSearch, setParentSearch] = createSignal('')
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
// Состояние для редактирования body
const [showBodyEditor, setShowBodyEditor] = createSignal(false)
const [bodyContent, setBodyContent] = createSignal('')
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
const [saving, setSaving] = createSignal(false)
// Инициализация формы при открытии
createEffect(() => {
if (props.isOpen && props.topic) {
console.log('[TopicEditModal] Initializing with topic:', props.topic)
setFormData({
id: props.topic.id,
title: props.topic.title || '',
slug: props.topic.slug || '',
body: props.topic.body || '',
community: selectedCommunity() || 0,
parent_ids: props.topic.parent_ids || []
})
setBodyContent(props.topic.body || '')
updateAvailableParents(selectedCommunity() || 0)
2025-06-30 18:25:26 +00:00
}
})
2025-07-02 19:30:21 +00:00
// Обновление доступных родителей при смене сообщества
const updateAvailableParents = (communityId: number) => {
const allTopics = topics()
const currentTopicId = formData().id
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
// Фильтруем топики того же сообщества, исключая текущий топик
const filteredTopics = allTopics.filter(
(topic) => topic.community === communityId && topic.id !== currentTopicId
)
setAvailableParents(filteredTopics)
}
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
// Фильтрация родителей по поиску
const filteredParents = () => {
const search = parentSearch().toLowerCase()
if (!search) return availableParents()
return availableParents().filter(
(topic) => topic.title?.toLowerCase().includes(search) || topic.slug?.toLowerCase().includes(search)
)
}
// Обработка изменения сообщества
const handleCommunityChange = (e: Event) => {
const target = e.target as HTMLSelectElement
const communityId = Number.parseInt(target.value)
setFormData((prev) => ({
...prev,
community: communityId,
parent_ids: [] // Сбрасываем родителей при смене сообщества
}))
updateAvailableParents(communityId)
}
// Обработка изменения родителей
const handleParentToggle = (parentId: number) => {
setFormData((prev) => ({
...prev,
parent_ids: prev.parent_ids.includes(parentId)
? prev.parent_ids.filter((id) => id !== parentId)
: [...prev.parent_ids, parentId]
}))
}
// Обработка изменения полей формы
const handleFieldChange = (field: string, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value
}))
2025-06-30 18:25:26 +00:00
}
2025-07-02 19:30:21 +00:00
// Открытие редактора body
const handleOpenBodyEditor = () => {
setBodyContent(formData().body)
setShowBodyEditor(true)
}
// Сохранение body из редактора
const handleBodySave = (content: string) => {
setFormData((prev) => ({
...prev,
body: content
}))
setBodyContent(content)
setShowBodyEditor(false)
}
// Получение пути до корня для топика
const getTopicPath = (topicId: number): string => {
const topic = topics().find((t) => t.id === topicId)
if (!topic) return 'Неизвестный топик'
const community = getCommunityName(topic.community)
return `${community}${topic.title}`
}
// Сохранение изменений
const handleSave = async () => {
try {
setSaving(true)
const updatedTopic = {
...props.topic,
...formData()
}
console.log('[TopicEditModal] Saving topic:', updatedTopic)
// TODO: Здесь должен быть вызов API для сохранения
// await updateTopic(updatedTopic)
props.onSave(updatedTopic)
props.onClose()
} catch (error) {
console.error('[TopicEditModal] Error saving topic:', error)
props.onError?.(error instanceof Error ? error.message : 'Ошибка сохранения топика')
} finally {
setSaving(false)
}
2025-06-30 18:25:26 +00:00
}
return (
2025-07-02 19:30:21 +00:00
<>
<Modal
isOpen={props.isOpen && !showBodyEditor()}
onClose={props.onClose}
title="Редактирование топика"
size="large"
>
<div class={styles.form}>
{/* Основная информация */}
<div class={styles.section}>
<h3>Основная информация</h3>
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
<div class={styles.field}>
<label class={styles.label}>
Название:
<input
type="text"
class={styles.input}
value={formData().title}
onInput={(e) => handleFieldChange('title', e.currentTarget.value)}
placeholder="Введите название топика..."
/>
</label>
</div>
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
<div class={styles.field}>
<label class={styles.label}>
Slug:
<input
type="text"
class={styles.input}
value={formData().slug}
onInput={(e) => handleFieldChange('slug', e.currentTarget.value)}
placeholder="Введите slug топика..."
/>
</label>
</div>
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
<div class={styles.field}>
<label class={styles.label}>
Сообщество:
<select class={styles.select} value={formData().community} onChange={handleCommunityChange}>
<option value={0}>Выберите сообщество</option>
<For each={communities()}>
{(community) => <option value={community.id}>{community.name}</option>}
</For>
</select>
</label>
</div>
</div>
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
{/* Содержимое */}
<div class={styles.section}>
<h3>Содержимое</h3>
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
<div class={styles.field}>
<label class={styles.label}>Body:</label>
<div class={styles.bodyPreview} onClick={handleOpenBodyEditor}>
<Show when={formData().body}>
<div class={styles.bodyContent}>
{formData().body.length > 200
? `${formData().body.substring(0, 200)}...`
: formData().body}
</div>
</Show>
<Show when={!formData().body}>
<div class={styles.bodyPlaceholder}>Нет содержимого. Нажмите для редактирования.</div>
</Show>
<div class={styles.bodyHint}> Кликните для редактирования в полноэкранном редакторе</div>
</div>
</div>
</div>
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
{/* Родительские топики */}
<Show when={formData().community > 0}>
<div class={styles.section}>
<h3>Родительские топики</h3>
<div class={styles.field}>
<label class={styles.label}>
Поиск родителей:
<input
type="text"
class={styles.input}
value={parentSearch()}
onInput={(e) => setParentSearch(e.currentTarget.value)}
placeholder="Введите название для поиска..."
/>
</label>
</div>
2025-06-30 18:25:26 +00:00
2025-07-02 19:30:21 +00:00
<Show when={formData().parent_ids.length > 0}>
<div class={styles.selectedParents}>
<strong>Выбранные родители:</strong>
<ul class={styles.parentsList}>
<For each={formData().parent_ids}>
{(parentId) => (
<li class={styles.parentItem}>
<span>{getTopicPath(parentId)}</span>
<button
type="button"
class={styles.removeButton}
onClick={() => handleParentToggle(parentId)}
>
</button>
</li>
)}
</For>
</ul>
</div>
</Show>
<div class={styles.availableParents}>
<strong>Доступные родители:</strong>
<div class={styles.parentsGrid}>
<For each={filteredParents()}>
{(parent) => (
<label class={styles.parentCheckbox}>
<input
type="checkbox"
checked={formData().parent_ids.includes(parent.id)}
onChange={() => handleParentToggle(parent.id)}
/>
<span class={styles.parentLabel}>
<strong>{parent.title}</strong>
<br />
<small>{parent.slug}</small>
</span>
</label>
)}
</For>
</div>
<Show when={filteredParents().length === 0}>
<div class={styles.noParents}>
<Show when={parentSearch()}>Не найдено топиков по запросу "{parentSearch()}"</Show>
<Show when={!parentSearch()}>Нет доступных родительских топиков в этом сообществе</Show>
</div>
</Show>
</div>
</div>
</Show>
{/* Кнопки */}
<div class={modalStyles.modalActions}>
<button
type="button"
class={`${styles.button} ${styles.buttonSecondary}`}
onClick={props.onClose}
disabled={saving()}
>
Отмена
</button>
<button
type="button"
class={`${styles.button} ${styles.buttonPrimary}`}
onClick={handleSave}
disabled={saving() || !formData().title || !formData().slug || formData().community === 0}
>
{saving() ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
2025-06-30 18:25:26 +00:00
</div>
2025-07-02 19:30:21 +00:00
</Modal>
{/* Редактор body */}
<Modal
isOpen={showBodyEditor()}
onClose={() => setShowBodyEditor(false)}
title="Редактирование содержимого топика"
size="large"
>
<EditableCodePreview
content={bodyContent()}
maxHeight="85vh"
onContentChange={setBodyContent}
onSave={handleBodySave}
onCancel={() => setShowBodyEditor(false)}
placeholder="Введите содержимое топика..."
/>
</Modal>
</>
2025-06-30 18:25:26 +00:00
)
}