2025-06-30 22:20:48 +00:00
|
|
|
|
import { Component, createSignal, For, Show } from 'solid-js'
|
2025-07-01 06:32:22 +00:00
|
|
|
|
import styles from '../styles/Form.module.css'
|
2025-06-30 22:20:48 +00:00
|
|
|
|
import Button from '../ui/Button'
|
|
|
|
|
import Modal from '../ui/Modal'
|
|
|
|
|
|
|
|
|
|
interface Topic {
|
|
|
|
|
id: number
|
|
|
|
|
title: string
|
|
|
|
|
slug: string
|
|
|
|
|
parent_ids?: number[]
|
|
|
|
|
community: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TopicParentModalProps {
|
|
|
|
|
isOpen: boolean
|
|
|
|
|
onClose: () => void
|
|
|
|
|
topic: Topic | null
|
|
|
|
|
allTopics: Topic[]
|
|
|
|
|
onSave: (topic: Topic) => void
|
|
|
|
|
onError: (error: string) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const TopicParentModal: Component<TopicParentModalProps> = (props) => {
|
|
|
|
|
const [selectedParentId, setSelectedParentId] = createSignal<number | null>(null)
|
|
|
|
|
const [searchQuery, setSearchQuery] = createSignal('')
|
|
|
|
|
|
|
|
|
|
// Получаем текущего родителя при открытии модалки
|
|
|
|
|
const getCurrentParentId = (): number | null => {
|
|
|
|
|
const topic = props.topic
|
|
|
|
|
if (!topic || !topic.parent_ids || topic.parent_ids.length === 0) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return topic.parent_ids[topic.parent_ids.length - 1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Фильтрация доступных родителей
|
|
|
|
|
const getAvailableParents = () => {
|
|
|
|
|
const currentTopic = props.topic
|
|
|
|
|
if (!currentTopic) return []
|
|
|
|
|
|
2025-07-01 06:32:22 +00:00
|
|
|
|
return props.allTopics.filter((topic) => {
|
2025-06-30 22:20:48 +00:00
|
|
|
|
// Исключаем сам топик
|
|
|
|
|
if (topic.id === currentTopic.id) return false
|
|
|
|
|
|
|
|
|
|
// Исключаем топики из других сообществ
|
|
|
|
|
if (topic.community !== currentTopic.community) return false
|
|
|
|
|
|
|
|
|
|
// Исключаем дочерние топики (предотвращаем циклы)
|
|
|
|
|
if (isDescendant(currentTopic.id, topic.id)) return false
|
|
|
|
|
|
|
|
|
|
// Фильтр по поисковому запросу
|
|
|
|
|
const query = searchQuery().toLowerCase()
|
|
|
|
|
if (query && !topic.title.toLowerCase().includes(query)) return false
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Проверка, является ли топик потомком другого
|
|
|
|
|
const isDescendant = (ancestorId: number, descendantId: number): boolean => {
|
2025-07-01 06:32:22 +00:00
|
|
|
|
const descendant = props.allTopics.find((t) => t.id === descendantId)
|
2025-06-30 22:20:48 +00:00
|
|
|
|
if (!descendant || !descendant.parent_ids) return false
|
|
|
|
|
|
|
|
|
|
return descendant.parent_ids.includes(ancestorId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Получение пути к корню для отображения полного пути
|
|
|
|
|
const getTopicPath = (topicId: number): string => {
|
2025-07-01 06:32:22 +00:00
|
|
|
|
const topic = props.allTopics.find((t) => t.id === topicId)
|
2025-06-30 22:20:48 +00:00
|
|
|
|
if (!topic) return ''
|
|
|
|
|
|
|
|
|
|
if (!topic.parent_ids || topic.parent_ids.length === 0) {
|
|
|
|
|
return topic.title
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parentPath = getTopicPath(topic.parent_ids[topic.parent_ids.length - 1])
|
|
|
|
|
return `${parentPath} → ${topic.title}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Сохранение изменений
|
|
|
|
|
const handleSave = () => {
|
|
|
|
|
const currentTopic = props.topic
|
|
|
|
|
if (!currentTopic) return
|
|
|
|
|
|
|
|
|
|
const newParentId = selectedParentId()
|
|
|
|
|
let newParentIds: number[] = []
|
|
|
|
|
|
|
|
|
|
if (newParentId) {
|
2025-07-01 06:32:22 +00:00
|
|
|
|
const parentTopic = props.allTopics.find((t) => t.id === newParentId)
|
2025-06-30 22:20:48 +00:00
|
|
|
|
if (parentTopic) {
|
|
|
|
|
// Строим полный путь от корня до нового родителя
|
|
|
|
|
newParentIds = [...(parentTopic.parent_ids || []), newParentId]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedTopic: Topic = {
|
|
|
|
|
...currentTopic,
|
|
|
|
|
parent_ids: newParentIds
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
props.onSave(updatedTopic)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Инициализация при открытии
|
|
|
|
|
if (props.isOpen && props.topic) {
|
|
|
|
|
setSelectedParentId(getCurrentParentId())
|
|
|
|
|
setSearchQuery('')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
isOpen={props.isOpen}
|
|
|
|
|
onClose={props.onClose}
|
|
|
|
|
title={`Выбор родительской темы для "${props.topic?.title}"`}
|
|
|
|
|
>
|
|
|
|
|
<div class={styles.parentSelectorContainer}>
|
|
|
|
|
<div class={styles.searchSection}>
|
|
|
|
|
<label class={styles.label}>Поиск родительской темы:</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={searchQuery()}
|
|
|
|
|
onInput={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
placeholder="Введите название темы..."
|
|
|
|
|
class={styles.searchInput}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class={styles.currentSelection}>
|
|
|
|
|
<label class={styles.label}>Текущий родитель:</label>
|
|
|
|
|
<div class={styles.currentParent}>
|
2025-07-01 06:32:22 +00:00
|
|
|
|
<Show when={getCurrentParentId()} fallback={<span class={styles.noParent}>Корневая тема</span>}>
|
2025-06-30 22:20:48 +00:00
|
|
|
|
<span class={styles.parentPath}>
|
|
|
|
|
{getCurrentParentId() ? getTopicPath(getCurrentParentId()!) : ''}
|
|
|
|
|
</span>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class={styles.parentOptions}>
|
|
|
|
|
<label class={styles.label}>Выберите нового родителя:</label>
|
|
|
|
|
|
|
|
|
|
{/* Опция "Сделать корневой" */}
|
|
|
|
|
<div class={styles.parentOption}>
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
id="root-option"
|
|
|
|
|
name="parent"
|
|
|
|
|
checked={selectedParentId() === null}
|
|
|
|
|
onChange={() => setSelectedParentId(null)}
|
|
|
|
|
/>
|
|
|
|
|
<label for="root-option" class={styles.parentOptionLabel}>
|
|
|
|
|
<strong>🏠 Корневая тема</strong>
|
2025-07-01 06:32:22 +00:00
|
|
|
|
<div class={styles.parentDescription}>Переместить на верхний уровень иерархии</div>
|
2025-06-30 22:20:48 +00:00
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Доступные родители */}
|
|
|
|
|
<div class={styles.parentsList}>
|
|
|
|
|
<For each={getAvailableParents()}>
|
|
|
|
|
{(topic) => (
|
|
|
|
|
<div class={styles.parentOption}>
|
|
|
|
|
<input
|
|
|
|
|
type="radio"
|
|
|
|
|
id={`parent-${topic.id}`}
|
|
|
|
|
name="parent"
|
|
|
|
|
checked={selectedParentId() === topic.id}
|
|
|
|
|
onChange={() => setSelectedParentId(topic.id)}
|
|
|
|
|
/>
|
|
|
|
|
<label for={`parent-${topic.id}`} class={styles.parentOptionLabel}>
|
|
|
|
|
<strong>{topic.title}</strong>
|
|
|
|
|
<div class={styles.parentDescription}>
|
|
|
|
|
<span class={styles.topicId}>#{topic.id}</span>
|
|
|
|
|
<span class={styles.topicSlug}>{topic.slug}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<Show when={topic.parent_ids && topic.parent_ids.length > 0}>
|
2025-07-01 06:32:22 +00:00
|
|
|
|
<div class={styles.parentPath}>Путь: {getTopicPath(topic.id)}</div>
|
2025-06-30 22:20:48 +00:00
|
|
|
|
</Show>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Show when={getAvailableParents().length === 0 && searchQuery()}>
|
|
|
|
|
<div class={styles.noResults}>
|
|
|
|
|
Нет тем, соответствующих поисковому запросу "{searchQuery()}"
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class={styles.modalActions}>
|
|
|
|
|
<Button variant="secondary" onClick={props.onClose}>
|
|
|
|
|
Отмена
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
disabled={selectedParentId() === getCurrentParentId()}
|
|
|
|
|
>
|
|
|
|
|
Сохранить
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default TopicParentModal
|