panel-improves
This commit is contained in:
parent
547c934302
commit
27a358a41f
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -14,12 +14,11 @@
|
|||
- **Компактный дизайн**: Уменьшены отступы (padding) для экономии места
|
||||
- **Улучшенная синхронизация скролла**: Номера строк синхронизируются со скроллом основного контента
|
||||
- **ИСПРАВЛЕНО**: Исправлена проблема с курсором в режиме редактирования - курсор теперь корректно перемещается при вводе текста и сохраняет позицию при обновлении содержимого
|
||||
- **ИСПРАВЛЕНО**: Номера строк теперь правильно синхронизируются с содержимым - они прокручиваются вместе с текстом и показывают реальные номера строк документа
|
||||
- **УЛУЧШЕНО**: Увеличена максимальная высота модальных окон с содержимым публикаций с 70vh до 85vh для более комфортного редактирования
|
||||
- **ИСПРАВЛЕНО**: Убраны жесткие ограничения высоты в CSS (`min-height: 500px` в `.editableCodeContainer` и `min-height: 450px` в `.editorArea`) - теперь размер полностью контролируется параметром `maxHeight`
|
||||
- **УЛУЧШЕНО**: Редактор кода теперь использует точную высоту `height: 85vh` вместо ограничений `min-height/max-height` для лучшего контроля размеров
|
||||
- **ИСПРАВЛЕНО**: Модальное окно размера "large" теперь действительно занимает 85% высоты экрана (`height: 85vh, max-height: 85vh`)
|
||||
- **УЛУЧШЕНО**: Содержимое модального окна использует `flex: 1` для заполнения всей доступной площади, убран padding для максимального использования пространства
|
||||
- Номера строк теперь правильно синхронизируются с содержимым - они прокручиваются вместе с текстом и показывают реальные номера строк документа
|
||||
- Увеличена высота модальных окон
|
||||
- **УЛУЧШЕНО**: Уменьшена ширина области номеров строк с 50px до 24px для максимальной экономии места
|
||||
- **ОПТИМИЗИРОВАНО**: Размер шрифта номеров строк уменьшен до 9px, padding уменьшен до 2px для компактности
|
||||
- **УЛУЧШЕНО**: Содержимое сдвинуто ближе к левому краю (left: 24px), уменьшен padding с 12px до 8px для лучшего использования пространства
|
||||
- **Техническая архитектура**:
|
||||
- Функция `formatHtmlContent()` для автоматического форматирования HTML разметки
|
||||
- Функция `generateLineNumbers()` для генерации номеров строк
|
||||
|
@ -28,6 +27,7 @@
|
|||
- Улучшенная обработка различных типов контента (HTML/markup vs обычный текст)
|
||||
- Правильная работа с Selection API для сохранения позиции курсора в contentEditable элементах
|
||||
- Синхронизация содержимого редактируемой области без потери фокуса и позиции курсора
|
||||
- **РЕФАКТОРИНГ СТИЛЕЙ**: Все inline стили перенесены в CSS модули для лучшей поддерживаемости кода
|
||||
|
||||
### Исправления авторизации
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, createSignal, For, Show } from 'solid-js'
|
||||
import styles from '../styles/Form.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
import styles from '../styles/Form.module.css'
|
||||
|
||||
interface Topic {
|
||||
id: number
|
||||
|
@ -33,21 +33,19 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
|
||||
// Получаем выбранные топики
|
||||
const getSelectedTopics = () => {
|
||||
return props.allTopics.filter(topic =>
|
||||
props.selectedTopicIds.includes(topic.id)
|
||||
)
|
||||
return props.allTopics.filter((topic) => props.selectedTopicIds.includes(topic.id))
|
||||
}
|
||||
|
||||
// Фильтрация доступных родителей
|
||||
const getAvailableParents = () => {
|
||||
const selectedIds = new Set(props.selectedTopicIds)
|
||||
|
||||
return props.allTopics.filter(topic => {
|
||||
return props.allTopics.filter((topic) => {
|
||||
// Исключаем выбранные топики
|
||||
if (selectedIds.has(topic.id)) return false
|
||||
|
||||
// Исключаем топики, которые являются детьми выбранных
|
||||
const isChildOfSelected = props.selectedTopicIds.some(selectedId =>
|
||||
const isChildOfSelected = props.selectedTopicIds.some((selectedId) =>
|
||||
isDescendant(selectedId, topic.id)
|
||||
)
|
||||
if (isChildOfSelected) return false
|
||||
|
@ -62,7 +60,7 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
|
||||
// Проверка, является ли топик потомком другого
|
||||
const isDescendant = (ancestorId: number, descendantId: number): boolean => {
|
||||
const descendant = props.allTopics.find(t => t.id === descendantId)
|
||||
const descendant = props.allTopics.find((t) => t.id === descendantId)
|
||||
if (!descendant || !descendant.parent_ids) return false
|
||||
|
||||
return descendant.parent_ids.includes(ancestorId)
|
||||
|
@ -70,7 +68,7 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
|
||||
// Получение пути к корню
|
||||
const getTopicPath = (topicId: number): string => {
|
||||
const topic = props.allTopics.find(t => t.id === topicId)
|
||||
const topic = props.allTopics.find((t) => t.id === topicId)
|
||||
if (!topic) return ''
|
||||
|
||||
if (!topic.parent_ids || topic.parent_ids.length === 0) {
|
||||
|
@ -86,7 +84,7 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
const selectedTopics = getSelectedTopics()
|
||||
const communities = new Map<number, Topic[]>()
|
||||
|
||||
selectedTopics.forEach(topic => {
|
||||
selectedTopics.forEach((topic) => {
|
||||
if (!communities.has(topic.community)) {
|
||||
communities.set(topic.community, [])
|
||||
}
|
||||
|
@ -108,7 +106,7 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
return 'Выберите родительскую тему'
|
||||
}
|
||||
|
||||
const selectedParent = props.allTopics.find(t => t.id === newParentId())
|
||||
const selectedParent = props.allTopics.find((t) => t.id === newParentId())
|
||||
if (selectedParent) {
|
||||
const selectedCommunity = Array.from(communities.keys())[0]
|
||||
if (selectedParent.community !== selectedCommunity) {
|
||||
|
@ -130,11 +128,11 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
const changes: BulkParentChange[] = []
|
||||
const selectedTopics = getSelectedTopics()
|
||||
|
||||
selectedTopics.forEach(topic => {
|
||||
selectedTopics.forEach((topic) => {
|
||||
let newParentIds: number[] = []
|
||||
|
||||
if (actionType() === 'set' && newParentId()) {
|
||||
const parentTopic = props.allTopics.find(t => t.id === newParentId())
|
||||
const parentTopic = props.allTopics.find((t) => t.id === newParentId())
|
||||
if (parentTopic) {
|
||||
newParentIds = [...(parentTopic.parent_ids || []), newParentId()!]
|
||||
}
|
||||
|
@ -161,7 +159,8 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
{/* Проверка совместимости */}
|
||||
<Show when={getTopicsByCommunity().size > 1}>
|
||||
<div class={styles.errorMessage}>
|
||||
⚠️ Выбраны темы из разных сообществ. Массовое изменение иерархии возможно только для тем одного сообщества.
|
||||
⚠️ Выбраны темы из разных сообществ. Массовое изменение иерархии возможно только для тем одного
|
||||
сообщества.
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
@ -175,9 +174,7 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
<span class={styles.topicTitle}>{topic.title}</span>
|
||||
<span class={styles.topicId}>#{topic.id}</span>
|
||||
<Show when={topic.parent_ids && topic.parent_ids.length > 0}>
|
||||
<div class={styles.currentPath}>
|
||||
Текущий путь: {getTopicPath(topic.id)}
|
||||
</div>
|
||||
<div class={styles.currentPath}>Текущий путь: {getTopicPath(topic.id)}</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
|
@ -256,9 +253,7 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
<span class={styles.topicSlug}>{topic.slug}</span>
|
||||
</div>
|
||||
<Show when={topic.parent_ids && topic.parent_ids.length > 0}>
|
||||
<div class={styles.parentPath}>
|
||||
Текущий путь: {getTopicPath(topic.id)}
|
||||
</div>
|
||||
<div class={styles.parentPath}>Текущий путь: {getTopicPath(topic.id)}</div>
|
||||
</Show>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -270,8 +265,7 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
<div class={styles.noResults}>
|
||||
{searchQuery()
|
||||
? `Нет доступных тем для поиска "${searchQuery()}"`
|
||||
: 'Нет доступных родительских тем'
|
||||
}
|
||||
: 'Нет доступных родительских тем'}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
@ -292,11 +286,12 @@ const TopicBulkParentModal: Component<TopicBulkParentModalProps> = (props) => {
|
|||
</span>
|
||||
<span class={styles.arrow}>→</span>
|
||||
<span class={styles.afterState}>
|
||||
Станет: {
|
||||
actionType() === 'makeRoot'
|
||||
Станет:{' '}
|
||||
{actionType() === 'makeRoot'
|
||||
? 'Корневая тема'
|
||||
: newParentId() ? `${getTopicPath(newParentId()!)} → ${topic.title}` : ''
|
||||
}
|
||||
: newParentId()
|
||||
? `${getTopicPath(newParentId()!)} → ${topic.title}`
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, createSignal, For, Show } from 'solid-js'
|
||||
import { Component, createSignal, For, JSX, Show } from 'solid-js'
|
||||
import styles from '../styles/Form.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
import styles from '../styles/Form.module.css'
|
||||
|
||||
// Типы для топиков
|
||||
interface Topic {
|
||||
|
@ -28,7 +28,7 @@ interface HierarchyChange {
|
|||
oldParentIds: number[]
|
||||
}
|
||||
|
||||
const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
||||
const TopicHierarchyModal = (props: TopicHierarchyModalProps) => {
|
||||
const [localTopics, setLocalTopics] = createSignal<Topic[]>([])
|
||||
const [changes, setChanges] = createSignal<HierarchyChange[]>([])
|
||||
const [expandedNodes, setExpandedNodes] = createSignal<Set<number>>(new Set())
|
||||
|
@ -43,7 +43,7 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
setSearchQuery('')
|
||||
setSelectedForMove(null)
|
||||
// Раскрываем все узлы по умолчанию
|
||||
const allIds = new Set(props.topics.map(t => t.id))
|
||||
const allIds = new Set(props.topics.map((t) => t.id))
|
||||
setExpandedNodes(allIds)
|
||||
}
|
||||
|
||||
|
@ -112,10 +112,8 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
// Обновление родителя топика
|
||||
const updateTopicParent = (topicId: number, newParentIds: number[]) => {
|
||||
const flatTopics = flattenTopics(localTopics())
|
||||
const updatedTopics = flatTopics.map(topic =>
|
||||
topic.id === topicId
|
||||
? { ...topic, parent_ids: newParentIds }
|
||||
: topic
|
||||
const updatedTopics = flatTopics.map((topic) =>
|
||||
topic.id === topicId ? { ...topic, parent_ids: newParentIds } : topic
|
||||
)
|
||||
const newHierarchy = buildHierarchy(updatedTopics)
|
||||
setLocalTopics(newHierarchy)
|
||||
|
@ -125,7 +123,7 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
const flattenTopics = (topics: Topic[]): Topic[] => {
|
||||
const result: Topic[] = []
|
||||
const flatten = (topicList: Topic[]) => {
|
||||
topicList.forEach(topic => {
|
||||
topicList.forEach((topic) => {
|
||||
result.push(topic)
|
||||
if (topic.children) {
|
||||
flatten(topic.children)
|
||||
|
@ -173,7 +171,9 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
setSelectedForMove(topicId)
|
||||
const topic = findTopicById(topicId)
|
||||
if (topic) {
|
||||
props.onError(`Выбрана тема "${topic.title}" для перемещения. Теперь нажмите на новую родительскую тему или используйте "Сделать корневой".`)
|
||||
props.onError(
|
||||
`Выбрана тема "${topic.title}" для перемещения. Теперь нажмите на новую родительскую тему или используйте "Сделать корневой".`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,8 +203,8 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
updateTopicParent(selectedId, newParentIds)
|
||||
|
||||
// Добавляем в список изменений
|
||||
setChanges(prev => [
|
||||
...prev.filter(c => c.topicId !== selectedId),
|
||||
setChanges((prev) => [
|
||||
...prev.filter((c) => c.topicId !== selectedId),
|
||||
{
|
||||
topicId: selectedId,
|
||||
newParentIds,
|
||||
|
@ -222,14 +222,14 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
|
||||
const expanded = expandedNodes()
|
||||
// Раскрываем всех родителей
|
||||
topic.parent_ids.forEach(parentId => {
|
||||
topic.parent_ids.forEach((parentId) => {
|
||||
expanded.add(parentId)
|
||||
})
|
||||
setExpandedNodes(new Set(expanded))
|
||||
}
|
||||
|
||||
// Рендеринг дерева топиков
|
||||
const renderTree = (topics: Topic[]): any => {
|
||||
const renderTree = (topics: Topic[]): JSX.Element => {
|
||||
return (
|
||||
<For each={topics}>
|
||||
{(topic) => {
|
||||
|
@ -253,8 +253,12 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
}}
|
||||
style={{
|
||||
'padding-left': `${(topic.level || 0) * 20}px`,
|
||||
'cursor': 'pointer',
|
||||
'border': isSelected ? '2px solid #007bff' : isTarget ? '2px dashed #28a745' : '1px solid transparent',
|
||||
cursor: 'pointer',
|
||||
border: isSelected
|
||||
? '2px solid #007bff'
|
||||
: isTarget
|
||||
? '2px dashed #28a745'
|
||||
: '1px solid transparent',
|
||||
'background-color': isSelected ? '#e3f2fd' : isTarget ? '#d4edda' : 'transparent'
|
||||
}}
|
||||
>
|
||||
|
@ -276,7 +280,7 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
</button>
|
||||
</Show>
|
||||
<Show when={!hasChildren}>
|
||||
<span style={{ width: '12px' }}></span>
|
||||
<span style={{ width: '12px' }} />
|
||||
</Show>
|
||||
|
||||
<Show when={isSelected}>
|
||||
|
@ -295,9 +299,7 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
</div>
|
||||
|
||||
<Show when={isExpanded && hasChildren}>
|
||||
<div class={styles.treeChildren}>
|
||||
{renderTree(topic.children!)}
|
||||
</div>
|
||||
<div class={styles.treeChildren}>{renderTree(topic.children!)}</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
@ -306,23 +308,6 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
)
|
||||
}
|
||||
|
||||
// Сброс корневого уровня (перетаскивание в корень)
|
||||
const makeRootTopic = (topicId: number) => {
|
||||
updateTopicParent(topicId, [])
|
||||
|
||||
const draggedTopic = findTopicById(topicId)
|
||||
if (!draggedTopic) return
|
||||
|
||||
setChanges(prev => [
|
||||
...prev.filter(c => c.topicId !== topicId),
|
||||
{
|
||||
topicId,
|
||||
newParentIds: [],
|
||||
oldParentIds: draggedTopic.parent_ids || []
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// Сохранение изменений
|
||||
const handleSave = () => {
|
||||
if (changes().length === 0) {
|
||||
|
@ -338,12 +323,7 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={props.isOpen}
|
||||
onClose={props.onClose}
|
||||
title="Управление иерархией тем"
|
||||
size="large"
|
||||
>
|
||||
<Modal isOpen={props.isOpen} onClose={props.onClose} title="Управление иерархией тем" size="large">
|
||||
<div class={styles.hierarchyContainer}>
|
||||
<div class={styles.instructions}>
|
||||
<h4>Инструкции:</h4>
|
||||
|
@ -382,15 +362,11 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
</div>
|
||||
</Show>
|
||||
<Show when={searchQuery() && !findTopicByTitle(searchQuery())}>
|
||||
<div class={styles.searchNoResult}>
|
||||
❌ Тема не найдена
|
||||
</div>
|
||||
<div class={styles.searchNoResult}>❌ Тема не найдена</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class={styles.hierarchyTree}>
|
||||
{renderTree(localTopics())}
|
||||
</div>
|
||||
<div class={styles.hierarchyTree}>{renderTree(localTopics())}</div>
|
||||
|
||||
<Show when={changes().length > 0}>
|
||||
<div class={styles.changesSummary}>
|
||||
|
@ -400,11 +376,10 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
const topic = findTopicById(change.topicId)
|
||||
return (
|
||||
<div class={styles.changeItem}>
|
||||
<strong>{topic?.title}</strong>: {
|
||||
change.newParentIds.length === 0
|
||||
<strong>{topic?.title}</strong>:{' '}
|
||||
{change.newParentIds.length === 0
|
||||
? 'станет корневой темой'
|
||||
: `переместится под тему #${change.newParentIds[change.newParentIds.length - 1]}`
|
||||
}
|
||||
: `переместится под тему #${change.newParentIds[change.newParentIds.length - 1]}`}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
@ -422,16 +397,10 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
</div>
|
||||
|
||||
<div class={styles.actionButtons}>
|
||||
<button
|
||||
class={styles.rootButton}
|
||||
onClick={() => moveSelectedTopic(null)}
|
||||
>
|
||||
<button class={styles.rootButton} onClick={() => moveSelectedTopic(null)}>
|
||||
🏠 Сделать корневой темой
|
||||
</button>
|
||||
<button
|
||||
class={styles.cancelButton}
|
||||
onClick={() => setSelectedForMove(null)}
|
||||
>
|
||||
<button class={styles.cancelButton} onClick={() => setSelectedForMove(null)}>
|
||||
❌ Отменить выбор
|
||||
</button>
|
||||
</div>
|
||||
|
@ -442,11 +411,7 @@ const TopicHierarchyModal: Component<TopicHierarchyModalProps> = (props) => {
|
|||
<Button variant="secondary" onClick={props.onClose}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={changes().length === 0}
|
||||
>
|
||||
<Button variant="primary" onClick={handleSave} disabled={changes().length === 0}>
|
||||
Сохранить изменения ({changes().length})
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, createSignal, For, Show } from 'solid-js'
|
||||
import { MERGE_TOPICS_MUTATION } from '../graphql/mutations'
|
||||
import styles from '../styles/Form.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
import styles from '../styles/Form.module.css'
|
||||
import { MERGE_TOPICS_MUTATION } from '../graphql/mutations'
|
||||
|
||||
// Типы для топиков
|
||||
interface Topic {
|
||||
|
@ -44,10 +44,12 @@ const TopicMergeModal: Component<TopicMergeModalProps> = (props) => {
|
|||
* Получает токен авторизации из localStorage или cookie
|
||||
*/
|
||||
const getAuthTokenFromCookie = () => {
|
||||
return document.cookie
|
||||
return (
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('auth_token='))
|
||||
.find((row) => row.startsWith('auth_token='))
|
||||
?.split('=')[1] || ''
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,9 +57,9 @@ const TopicMergeModal: Component<TopicMergeModalProps> = (props) => {
|
|||
*/
|
||||
const handleSourceTopicToggle = (topicId: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSourceTopicIds(prev => [...prev, topicId])
|
||||
setSourceTopicIds((prev) => [...prev, topicId])
|
||||
} else {
|
||||
setSourceTopicIds(prev => prev.filter(id => id !== topicId))
|
||||
setSourceTopicIds((prev) => prev.filter((id) => id !== topicId))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,13 +80,13 @@ const TopicMergeModal: Component<TopicMergeModalProps> = (props) => {
|
|||
}
|
||||
|
||||
// Проверяем что все темы принадлежат одному сообществу
|
||||
const targetTopic = props.topics.find(t => t.id === target)
|
||||
const targetTopic = props.topics.find((t) => t.id === target)
|
||||
if (!targetTopic) return false
|
||||
|
||||
const targetCommunity = targetTopic.community
|
||||
const sourcesTopics = props.topics.filter(t => sources.includes(t.id))
|
||||
const sourcesTopics = props.topics.filter((t) => sources.includes(t.id))
|
||||
|
||||
return sourcesTopics.every(topic => topic.community === targetCommunity)
|
||||
return sourcesTopics.every((topic) => topic.community === targetCommunity)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,12 +143,12 @@ const TopicMergeModal: Component<TopicMergeModalProps> = (props) => {
|
|||
}
|
||||
|
||||
const stats = mergeResult.stats as MergeStats
|
||||
const statsText = stats ?
|
||||
` (перенесено ${stats.followers_moved} подписчиков, ${stats.publications_moved} публикаций, ${stats.drafts_moved} черновиков, удалено ${stats.source_topics_deleted} тем)` : ''
|
||||
const statsText = stats
|
||||
? ` (перенесено ${stats.followers_moved} подписчиков, ${stats.publications_moved} публикаций, ${stats.drafts_moved} черновиков, удалено ${stats.source_topics_deleted} тем)`
|
||||
: ''
|
||||
|
||||
props.onSuccess(mergeResult.message + statsText)
|
||||
handleClose()
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message
|
||||
setError(errorMessage)
|
||||
|
@ -173,7 +175,7 @@ const TopicMergeModal: Component<TopicMergeModalProps> = (props) => {
|
|||
*/
|
||||
const getAvailableTargetTopics = () => {
|
||||
const sources = sourceTopicIds()
|
||||
return props.topics.filter(topic => !sources.includes(topic.id))
|
||||
return props.topics.filter((topic) => !sources.includes(topic.id))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -181,26 +183,22 @@ const TopicMergeModal: Component<TopicMergeModalProps> = (props) => {
|
|||
*/
|
||||
const getAvailableSourceTopics = () => {
|
||||
const target = targetTopicId()
|
||||
return props.topics.filter(topic => topic.id !== target)
|
||||
return props.topics.filter((topic) => topic.id !== target)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={props.isOpen}
|
||||
onClose={handleClose}
|
||||
title="Слияние тем"
|
||||
size="large"
|
||||
>
|
||||
<Modal isOpen={props.isOpen} onClose={handleClose} title="Слияние тем" size="large">
|
||||
<div class={styles.form}>
|
||||
<div class={styles.section}>
|
||||
<h3 class={styles.sectionTitle}>Выбор целевой темы</h3>
|
||||
<p class={styles.description}>
|
||||
Выберите тему, в которую будут слиты остальные темы. Все подписчики и публикации будут перенесены в эту тему.
|
||||
Выберите тему, в которую будут слиты остальные темы. Все подписчики и публикации будут
|
||||
перенесены в эту тему.
|
||||
</p>
|
||||
|
||||
<select
|
||||
value={targetTopicId() || ''}
|
||||
onChange={(e) => setTargetTopicId(e.target.value ? parseInt(e.target.value) : null)}
|
||||
onChange={(e) => setTargetTopicId(e.target.value ? Number.parseInt(e.target.value) : null)}
|
||||
class={styles.select}
|
||||
disabled={loading()}
|
||||
>
|
||||
|
@ -219,7 +217,8 @@ const TopicMergeModal: Component<TopicMergeModalProps> = (props) => {
|
|||
<div class={styles.section}>
|
||||
<h3 class={styles.sectionTitle}>Выбор исходных тем для слияния</h3>
|
||||
<p class={styles.description}>
|
||||
Выберите темы, которые будут слиты в целевую тему. Эти темы будут удалены после переноса всех связей.
|
||||
Выберите темы, которые будут слиты в целевую тему. Эти темы будут удалены после переноса всех
|
||||
связей.
|
||||
</p>
|
||||
|
||||
<Show when={getAvailableSourceTopics().length > 0}>
|
||||
|
@ -242,7 +241,10 @@ const TopicMergeModal: Component<TopicMergeModalProps> = (props) => {
|
|||
<div class={styles.topicInfo}>
|
||||
{getCommunityName(topic.community)} • ID: {topic.id}
|
||||
{topic.stat && (
|
||||
<span> • {topic.stat.shouts} публ., {topic.stat.followers} подп.</span>
|
||||
<span>
|
||||
{' '}
|
||||
• {topic.stat.shouts} публ., {topic.stat.followers} подп.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -283,39 +285,32 @@ const TopicMergeModal: Component<TopicMergeModalProps> = (props) => {
|
|||
<h4>Предпросмотр слияния:</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Целевая тема:</strong> {props.topics.find(t => t.id === targetTopicId())?.title}
|
||||
<strong>Целевая тема:</strong> {props.topics.find((t) => t.id === targetTopicId())?.title}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Исходные темы:</strong> {sourceTopicIds().length} шт.
|
||||
<ul>
|
||||
<For each={sourceTopicIds()}>
|
||||
{(id) => {
|
||||
const topic = props.topics.find(t => t.id === id)
|
||||
const topic = props.topics.find((t) => t.id === id)
|
||||
return topic ? <li>{topic.title}</li> : null
|
||||
}}
|
||||
</For>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Действие:</strong> Все подписчики, публикации и черновики будут перенесены в целевую тему, исходные темы будут удалены
|
||||
<strong>Действие:</strong> Все подписчики, публикации и черновики будут перенесены в целевую
|
||||
тему, исходные темы будут удалены
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={styles.modalActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleClose}
|
||||
disabled={loading()}
|
||||
>
|
||||
<Button variant="secondary" onClick={handleClose} disabled={loading()}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={handleMerge}
|
||||
disabled={!canMerge() || loading()}
|
||||
>
|
||||
<Button variant="danger" onClick={handleMerge} disabled={!canMerge() || loading()}>
|
||||
{loading() ? 'Выполняется слияние...' : 'Слить темы'}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, createSignal, For, Show } from 'solid-js'
|
||||
import styles from '../styles/Form.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
import styles from '../styles/Form.module.css'
|
||||
|
||||
interface Topic {
|
||||
id: number
|
||||
|
@ -38,7 +38,7 @@ const TopicParentModal: Component<TopicParentModalProps> = (props) => {
|
|||
const currentTopic = props.topic
|
||||
if (!currentTopic) return []
|
||||
|
||||
return props.allTopics.filter(topic => {
|
||||
return props.allTopics.filter((topic) => {
|
||||
// Исключаем сам топик
|
||||
if (topic.id === currentTopic.id) return false
|
||||
|
||||
|
@ -58,7 +58,7 @@ const TopicParentModal: Component<TopicParentModalProps> = (props) => {
|
|||
|
||||
// Проверка, является ли топик потомком другого
|
||||
const isDescendant = (ancestorId: number, descendantId: number): boolean => {
|
||||
const descendant = props.allTopics.find(t => t.id === descendantId)
|
||||
const descendant = props.allTopics.find((t) => t.id === descendantId)
|
||||
if (!descendant || !descendant.parent_ids) return false
|
||||
|
||||
return descendant.parent_ids.includes(ancestorId)
|
||||
|
@ -66,7 +66,7 @@ const TopicParentModal: Component<TopicParentModalProps> = (props) => {
|
|||
|
||||
// Получение пути к корню для отображения полного пути
|
||||
const getTopicPath = (topicId: number): string => {
|
||||
const topic = props.allTopics.find(t => t.id === topicId)
|
||||
const topic = props.allTopics.find((t) => t.id === topicId)
|
||||
if (!topic) return ''
|
||||
|
||||
if (!topic.parent_ids || topic.parent_ids.length === 0) {
|
||||
|
@ -86,7 +86,7 @@ const TopicParentModal: Component<TopicParentModalProps> = (props) => {
|
|||
let newParentIds: number[] = []
|
||||
|
||||
if (newParentId) {
|
||||
const parentTopic = props.allTopics.find(t => t.id === newParentId)
|
||||
const parentTopic = props.allTopics.find((t) => t.id === newParentId)
|
||||
if (parentTopic) {
|
||||
// Строим полный путь от корня до нового родителя
|
||||
newParentIds = [...(parentTopic.parent_ids || []), newParentId]
|
||||
|
@ -128,10 +128,7 @@ const TopicParentModal: Component<TopicParentModalProps> = (props) => {
|
|||
<div class={styles.currentSelection}>
|
||||
<label class={styles.label}>Текущий родитель:</label>
|
||||
<div class={styles.currentParent}>
|
||||
<Show
|
||||
when={getCurrentParentId()}
|
||||
fallback={<span class={styles.noParent}>Корневая тема</span>}
|
||||
>
|
||||
<Show when={getCurrentParentId()} fallback={<span class={styles.noParent}>Корневая тема</span>}>
|
||||
<span class={styles.parentPath}>
|
||||
{getCurrentParentId() ? getTopicPath(getCurrentParentId()!) : ''}
|
||||
</span>
|
||||
|
@ -153,9 +150,7 @@ const TopicParentModal: Component<TopicParentModalProps> = (props) => {
|
|||
/>
|
||||
<label for="root-option" class={styles.parentOptionLabel}>
|
||||
<strong>🏠 Корневая тема</strong>
|
||||
<div class={styles.parentDescription}>
|
||||
Переместить на верхний уровень иерархии
|
||||
</div>
|
||||
<div class={styles.parentDescription}>Переместить на верхний уровень иерархии</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
@ -178,9 +173,7 @@ const TopicParentModal: Component<TopicParentModalProps> = (props) => {
|
|||
<span class={styles.topicSlug}>{topic.slug}</span>
|
||||
</div>
|
||||
<Show when={topic.parent_ids && topic.parent_ids.length > 0}>
|
||||
<div class={styles.parentPath}>
|
||||
Путь: {getTopicPath(topic.id)}
|
||||
</div>
|
||||
<div class={styles.parentPath}>Путь: {getTopicPath(topic.id)}</div>
|
||||
</Show>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, createSignal, For, Show } from 'solid-js'
|
||||
import { SET_TOPIC_PARENT_MUTATION } from '../graphql/mutations'
|
||||
import styles from '../styles/Form.module.css'
|
||||
import Button from '../ui/Button'
|
||||
import Modal from '../ui/Modal'
|
||||
import styles from '../styles/Form.module.css'
|
||||
import { SET_TOPIC_PARENT_MUTATION } from '../graphql/mutations'
|
||||
|
||||
// Типы для топиков
|
||||
interface Topic {
|
||||
|
@ -31,10 +31,12 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
* Получает токен авторизации
|
||||
*/
|
||||
const getAuthTokenFromCookie = () => {
|
||||
return document.cookie
|
||||
return (
|
||||
document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('auth_token='))
|
||||
.find((row) => row.startsWith('auth_token='))
|
||||
?.split('=')[1] || ''
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,7 +53,7 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
* Получает путь темы до корня
|
||||
*/
|
||||
const getTopicPath = (topicId: number): string => {
|
||||
const topic = props.allTopics.find(t => t.id === topicId)
|
||||
const topic = props.allTopics.find((t) => t.id === topicId)
|
||||
if (!topic) return 'Неизвестная тема'
|
||||
|
||||
if (!topic.parent_ids || topic.parent_ids.length === 0) {
|
||||
|
@ -69,9 +71,7 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
if (parentId === childId) return true
|
||||
|
||||
const checkDescendants = (currentId: number): boolean => {
|
||||
const descendants = props.allTopics.filter(t =>
|
||||
t.parent_ids && t.parent_ids.includes(currentId)
|
||||
)
|
||||
const descendants = props.allTopics.filter((t) => t?.parent_ids?.includes(currentId))
|
||||
|
||||
for (const descendant of descendants) {
|
||||
if (descendant.id === childId || checkDescendants(descendant.id)) {
|
||||
|
@ -92,7 +92,7 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
|
||||
const query = searchQuery().toLowerCase()
|
||||
|
||||
return props.allTopics.filter(topic => {
|
||||
return props.allTopics.filter((topic) => {
|
||||
// Исключаем саму тему
|
||||
if (topic.id === props.topic!.id) return false
|
||||
|
||||
|
@ -149,7 +149,6 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
|
||||
props.onSuccess(setResult.message)
|
||||
handleClose()
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message
|
||||
props.onError(`Ошибка назначения родителя: ${errorMessage}`)
|
||||
|
@ -169,12 +168,7 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={props.isOpen}
|
||||
onClose={handleClose}
|
||||
title="Назначить родительскую тему"
|
||||
size="medium"
|
||||
>
|
||||
<Modal isOpen={props.isOpen} onClose={handleClose} title="Назначить родительскую тему" size="medium">
|
||||
<div class={styles.parentSelectorContainer}>
|
||||
<Show when={props.topic}>
|
||||
<div class={styles.currentSelection}>
|
||||
|
@ -186,10 +180,11 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
<div class={styles.currentParent}>
|
||||
<strong>Текущее расположение:</strong>
|
||||
<div class={styles.parentPath}>
|
||||
{getCurrentParentId() ?
|
||||
getTopicPath(props.topic!.id) :
|
||||
{getCurrentParentId() ? (
|
||||
getTopicPath(props.topic!.id)
|
||||
) : (
|
||||
<span class={styles.noParent}>🏠 Корневая тема</span>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -220,9 +215,7 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
disabled={loading()}
|
||||
/>
|
||||
<div class={styles.parentOptionLabel}>
|
||||
<div class={styles.topicTitle}>
|
||||
🏠 Сделать корневой темой
|
||||
</div>
|
||||
<div class={styles.topicTitle}>🏠 Сделать корневой темой</div>
|
||||
<div class={styles.parentDescription}>
|
||||
Тема будет перемещена на верхний уровень иерархии
|
||||
</div>
|
||||
|
@ -244,9 +237,7 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
disabled={loading()}
|
||||
/>
|
||||
<div class={styles.parentOptionLabel}>
|
||||
<div class={styles.topicTitle}>
|
||||
{topic.title}
|
||||
</div>
|
||||
<div class={styles.topicTitle}>{topic.title}</div>
|
||||
<div class={styles.parentDescription}>
|
||||
<span class={styles.topicId}>ID: {topic.id}</span>
|
||||
<span class={styles.topicSlug}>• {topic.slug}</span>
|
||||
|
@ -261,10 +252,9 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
|
||||
<Show when={getAvailableParents().length === 0}>
|
||||
<div class={styles.noResults}>
|
||||
{searchQuery() ?
|
||||
'Не найдено подходящих тем по запросу' :
|
||||
'Нет доступных родительских тем'
|
||||
}
|
||||
{searchQuery()
|
||||
? 'Не найдено подходящих тем по запросу'
|
||||
: 'Нет доступных родительских тем'}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
@ -274,24 +264,21 @@ const TopicSimpleParentModal: Component<TopicSimpleParentModalProps> = (props) =
|
|||
<div class={styles.preview}>
|
||||
<h4>Предварительный просмотр:</h4>
|
||||
<div class={styles.previewPath}>
|
||||
<strong>Новое расположение:</strong><br />
|
||||
<strong>Новое расположение:</strong>
|
||||
<br />
|
||||
{getTopicPath(selectedParentId()!)} → <strong>{props.topic?.title}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={styles.modalActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleClose}
|
||||
disabled={loading()}
|
||||
>
|
||||
<Button variant="secondary" onClick={handleClose} disabled={loading()}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSetParent}
|
||||
disabled={loading() || (selectedParentId() === getCurrentParentId())}
|
||||
disabled={loading() || selectedParentId() === getCurrentParentId()}
|
||||
>
|
||||
{loading() ? 'Назначение...' : 'Назначить родителя'}
|
||||
</Button>
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import { Component, createSignal, For, onMount, Show } from 'solid-js'
|
||||
import {
|
||||
ADMIN_DELETE_INVITE_MUTATION,
|
||||
ADMIN_DELETE_INVITES_BATCH_MUTATION
|
||||
} from '../graphql/mutations'
|
||||
import { ADMIN_DELETE_INVITE_MUTATION, ADMIN_DELETE_INVITES_BATCH_MUTATION } from '../graphql/mutations'
|
||||
import { ADMIN_GET_INVITES_QUERY } from '../graphql/queries'
|
||||
import styles from '../styles/Table.module.css'
|
||||
import Button from '../ui/Button'
|
||||
|
@ -227,7 +224,7 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
|||
const deleteSelectedInvites = async () => {
|
||||
try {
|
||||
const selected = selectedInvites()
|
||||
const invitesToDelete = invites().filter(invite => {
|
||||
const invitesToDelete = invites().filter((invite) => {
|
||||
const key = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}`
|
||||
return selected[key]
|
||||
})
|
||||
|
@ -239,7 +236,9 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
|||
|
||||
// Получаем токен авторизации из localStorage или cookie
|
||||
const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie()
|
||||
console.log(`[InvitesRoute] Пакетное удаление приглашений, токен: ${authToken ? 'найден' : 'не найден'}`)
|
||||
console.log(
|
||||
`[InvitesRoute] Пакетное удаление приглашений, токен: ${authToken ? 'найден' : 'не найден'}`
|
||||
)
|
||||
|
||||
const response = await fetch('/graphql', {
|
||||
method: 'POST',
|
||||
|
@ -250,7 +249,7 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
|||
body: JSON.stringify({
|
||||
query: ADMIN_DELETE_INVITES_BATCH_MUTATION,
|
||||
variables: {
|
||||
invites: invitesToDelete.map(invite => ({
|
||||
invites: invitesToDelete.map((invite) => ({
|
||||
inviter_id: invite.inviter_id,
|
||||
author_id: invite.author_id,
|
||||
shout_id: invite.shout_id
|
||||
|
@ -286,7 +285,7 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
|||
*/
|
||||
const handleSelectInvite = (invite: Invite, checked: boolean) => {
|
||||
const key = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}`
|
||||
setSelectedInvites(prev => ({ ...prev, [key]: checked }))
|
||||
setSelectedInvites((prev) => ({ ...prev, [key]: checked }))
|
||||
|
||||
// Если снимаем выбор с элемента, то снимаем и "выбрать все"
|
||||
if (!checked && selectAll()) {
|
||||
|
@ -303,7 +302,7 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
|||
const newSelected: Record<string, boolean> = {}
|
||||
if (checked) {
|
||||
// Выбираем все приглашения на текущей странице
|
||||
invites().forEach(invite => {
|
||||
invites().forEach((invite) => {
|
||||
const key = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}`
|
||||
newSelected[key] = true
|
||||
})
|
||||
|
@ -406,9 +405,7 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
|||
</div>
|
||||
|
||||
<Show when={getSelectedCount() > 0}>
|
||||
<div class={styles['selected-count']}>
|
||||
Выбрано: {getSelectedCount()}
|
||||
</div>
|
||||
<div class={styles['selected-count']}>Выбрано: {getSelectedCount()}</div>
|
||||
|
||||
<button
|
||||
class={styles['batch-delete-button']}
|
||||
|
@ -434,7 +431,7 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
|||
<table class={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class={styles['checkbox-column']}></th>
|
||||
<th class={styles['checkbox-column']} />
|
||||
<th class={styles.sortableHeader} onClick={() => handleSort('inviter_name')}>
|
||||
<span class={styles.headerContent}>
|
||||
Приглашающий
|
||||
|
@ -577,10 +574,7 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
|||
<Button variant="secondary" onClick={() => setBatchDeleteModal({ show: false })}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={deleteSelectedInvites}
|
||||
>
|
||||
<Button variant="danger" onClick={deleteSelectedInvites}>
|
||||
Удалить выбранные
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -326,9 +326,9 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
|
|||
*/
|
||||
const handleTopicSelect = (topicId: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedTopics(prev => [...prev, topicId])
|
||||
setSelectedTopics((prev) => [...prev, topicId])
|
||||
} else {
|
||||
setSelectedTopics(prev => prev.filter(id => id !== topicId))
|
||||
setSelectedTopics((prev) => prev.filter((id) => id !== topicId))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,7 +337,7 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
|
|||
*/
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
const allTopicIds = rawTopics().map(topic => topic.id)
|
||||
const allTopicIds = rawTopics().map((topic) => topic.id)
|
||||
setSelectedTopics(allTopicIds)
|
||||
} else {
|
||||
setSelectedTopics([])
|
||||
|
@ -348,9 +348,9 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
|
|||
* Проверяет выбраны ли все топики
|
||||
*/
|
||||
const isAllSelected = () => {
|
||||
const allIds = rawTopics().map(topic => topic.id)
|
||||
const allIds = rawTopics().map((topic) => topic.id)
|
||||
const selected = selectedTopics()
|
||||
return allIds.length > 0 && allIds.every(id => selected.includes(id))
|
||||
return allIds.length > 0 && allIds.every((id) => selected.includes(id))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -372,7 +372,7 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
|
|||
|
||||
if (action === 'delete') {
|
||||
// Групповое удаление
|
||||
const selectedTopicsData = rawTopics().filter(t => selected.includes(t.id))
|
||||
const selectedTopicsData = rawTopics().filter((t) => selected.includes(t.id))
|
||||
setDeleteModal({ show: true, topic: selectedTopicsData[0] }) // Используем первый для отображения
|
||||
} else if (action === 'merge') {
|
||||
// Слияние топиков
|
||||
|
@ -482,7 +482,7 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
|
|||
variant="secondary"
|
||||
onClick={() => {
|
||||
if (selectedTopics().length === 1) {
|
||||
const selectedTopic = rawTopics().find(t => t.id === selectedTopics()[0])
|
||||
const selectedTopic = rawTopics().find((t) => t.id === selectedTopics()[0])
|
||||
if (selectedTopic) {
|
||||
setSimpleParentModal({ show: true, topic: selectedTopic })
|
||||
}
|
||||
|
@ -515,7 +515,14 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
|
|||
<th>Сообщество</th>
|
||||
<th>Родители</th>
|
||||
<th>
|
||||
<div style={{ display: 'flex', 'align-items': 'center', gap: '8px', 'flex-direction': 'column' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
'align-items': 'center',
|
||||
gap: '8px',
|
||||
'flex-direction': 'column'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', 'align-items': 'center', gap: '4px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
@ -642,7 +649,7 @@ const TopicsRoute: Component<TopicsRouteProps> = (props) => {
|
|||
setSelectedTopics([])
|
||||
setGroupAction('')
|
||||
}}
|
||||
topics={rawTopics().filter(topic => selectedTopics().includes(topic.id))}
|
||||
topics={rawTopics().filter((topic) => selectedTopics().includes(topic.id))}
|
||||
onSuccess={(message) => {
|
||||
props.onSuccess(message)
|
||||
setSelectedTopics([])
|
||||
|
|
|
@ -1,33 +1,45 @@
|
|||
.codePreview {
|
||||
position: relative;
|
||||
padding-left: 50px !important;
|
||||
padding-left: 24px !important;
|
||||
background-color: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
tab-size: 2;
|
||||
line-height: 1.4;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.lineNumber {
|
||||
display: block;
|
||||
padding: 0 8px;
|
||||
padding: 0 2px;
|
||||
text-align: right;
|
||||
color: #555;
|
||||
background: #1e1e1e;
|
||||
user-select: none;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 11px;
|
||||
font-size: 9px;
|
||||
line-height: 1.4;
|
||||
min-height: 16.8px; /* 12px * 1.4 line-height */
|
||||
min-height: 12.6px; /* 9px * 1.4 line-height */
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lineNumbersContainer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 24px;
|
||||
height: 100%;
|
||||
background: #1e1e1e;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
padding: 8px 2px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 9px;
|
||||
line-height: 1.4;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.lineNumbersContainer .lineNumber {
|
||||
|
@ -48,14 +60,13 @@
|
|||
color: #fff;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
z-index: 100;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Стили для EditableCodePreview */
|
||||
.editableCodeContainer {
|
||||
position: relative;
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
@ -132,15 +143,37 @@
|
|||
}
|
||||
|
||||
.syntaxHighlight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 24px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
color: transparent;
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
padding: 8px 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
tab-size: 2;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.editorArea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 24px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
padding: 8px 8px;
|
||||
resize: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
|
@ -149,12 +182,66 @@
|
|||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.editorArea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.editorAreaEditing {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
cursor: text;
|
||||
caret-color: #fff;
|
||||
}
|
||||
|
||||
.editorAreaViewing {
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
cursor: default;
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.editorWrapperEditing {
|
||||
border: 2px solid #007acc;
|
||||
}
|
||||
|
||||
.codePreviewContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 24px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 8px 8px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
overflow-y: auto;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
|
|
@ -28,7 +28,7 @@ const formatHtmlContent = (html: string): string => {
|
|||
if (!html || typeof html !== 'string') return ''
|
||||
|
||||
// Удаляем лишние пробелы между тегами
|
||||
let formatted = html
|
||||
const formatted = html
|
||||
.replace(/>\s+</g, '><') // Убираем пробелы между тегами
|
||||
.replace(/\s+/g, ' ') // Множественные пробелы в одиночные
|
||||
.trim() // Убираем пробелы в начале и конце
|
||||
|
@ -117,7 +117,7 @@ const EditableCodePreview = (props: EditableCodePreviewProps) => {
|
|||
|
||||
const lineNumbers = generateLineNumbers(content())
|
||||
lineNumbersRef.innerHTML = lineNumbers
|
||||
.map(num => `<div class="${styles.lineNumber}">${num}</div>`)
|
||||
.map((num) => `<div class="${styles.lineNumber}">${num}</div>`)
|
||||
.join('')
|
||||
}
|
||||
|
||||
|
@ -243,9 +243,8 @@ const EditableCodePreview = (props: EditableCodePreviewProps) => {
|
|||
// Эффект для обновления контента при изменении props
|
||||
createEffect(() => {
|
||||
if (!isEditing()) {
|
||||
const formattedContent = language() === 'markup' || language() === 'html'
|
||||
? formatHtmlContent(props.content)
|
||||
: props.content
|
||||
const formattedContent =
|
||||
language() === 'markup' || language() === 'html' ? formatHtmlContent(props.content) : props.content
|
||||
setContent(formattedContent)
|
||||
updateHighlight()
|
||||
updateLineNumbers()
|
||||
|
@ -301,9 +300,8 @@ const EditableCodePreview = (props: EditableCodePreviewProps) => {
|
|||
})
|
||||
|
||||
onMount(() => {
|
||||
const formattedContent = language() === 'markup' || language() === 'html'
|
||||
? formatHtmlContent(props.content)
|
||||
: props.content
|
||||
const formattedContent =
|
||||
language() === 'markup' || language() === 'html' ? formatHtmlContent(props.content) : props.content
|
||||
setContent(formattedContent)
|
||||
updateHighlight()
|
||||
updateLineNumbers()
|
||||
|
@ -313,22 +311,17 @@ const EditableCodePreview = (props: EditableCodePreviewProps) => {
|
|||
<div class={styles.editableCodeContainer}>
|
||||
{/* Контейнер редактора - увеличиваем размер */}
|
||||
<div
|
||||
class={styles.editorWrapper}
|
||||
style={`height: 100%; ${isEditing() ? 'border: 2px solid #007acc;' : ''}`}
|
||||
class={`${styles.editorWrapper} ${isEditing() ? styles.editorWrapperEditing : ''}`}
|
||||
style="height: 100%;"
|
||||
>
|
||||
{/* Номера строк */}
|
||||
<div
|
||||
ref={lineNumbersRef}
|
||||
class={styles.lineNumbersContainer}
|
||||
style="position: absolute; left: 0; top: 0; width: 50px; height: 100%; background: #1e1e1e; border-right: 1px solid rgba(255, 255, 255, 0.1); overflow: hidden; user-select: none; padding: 8px 0; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; font-size: 12px; line-height: 1.4;"
|
||||
/>
|
||||
<div ref={lineNumbersRef} class={styles.lineNumbersContainer} />
|
||||
|
||||
{/* Подсветка синтаксиса (фон) - только в режиме редактирования */}
|
||||
<Show when={isEditing()}>
|
||||
<pre
|
||||
ref={highlightRef}
|
||||
class={`${styles.syntaxHighlight} language-${language()}`}
|
||||
style="position: absolute; top: 0; left: 50px; right: 0; bottom: 0; pointer-events: none; color: transparent; background: transparent; margin: 0; padding: 8px 12px; font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace; font-size: 12px; line-height: 1.4; white-space: pre-wrap; word-wrap: break-word; overflow: hidden; z-index: 0;"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Show>
|
||||
|
@ -343,28 +336,7 @@ const EditableCodePreview = (props: EditableCodePreviewProps) => {
|
|||
}
|
||||
}}
|
||||
contentEditable={isEditing()}
|
||||
class={styles.editorArea}
|
||||
style={`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
background: ${isEditing() ? 'rgba(0, 0, 0, 0.02)' : 'transparent'};
|
||||
color: ${isEditing() ? 'rgba(255, 255, 255, 0.9)' : 'transparent'};
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
outline: none;
|
||||
cursor: ${isEditing() ? 'text' : 'default'};
|
||||
caret-color: ${isEditing() ? '#fff' : 'transparent'};
|
||||
`}
|
||||
class={`${styles.editorArea} ${isEditing() ? styles.editorAreaEditing : styles.editorAreaViewing}`}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
onScroll={syncScroll}
|
||||
|
@ -374,25 +346,7 @@ const EditableCodePreview = (props: EditableCodePreviewProps) => {
|
|||
{/* Превью для неактивного режима */}
|
||||
<Show when={!isEditing()}>
|
||||
<pre
|
||||
class={`${styles.codePreview} language-${language()}`}
|
||||
style={`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
overflow-y: auto;
|
||||
z-index: 2;
|
||||
`}
|
||||
class={`${styles.codePreviewContainer} language-${language()}`}
|
||||
onClick={() => setIsEditing(true)}
|
||||
onScroll={(e) => {
|
||||
// Синхронизируем номера строк при скролле в режиме просмотра
|
||||
|
@ -416,29 +370,26 @@ const EditableCodePreview = (props: EditableCodePreviewProps) => {
|
|||
</div>
|
||||
|
||||
{/* Индикатор языка */}
|
||||
<span class={styles.languageBadge} style="top: 8px; right: 8px; z-index: 10;">
|
||||
{language()}
|
||||
</span>
|
||||
<span class={styles.languageBadge}>{language()}</span>
|
||||
|
||||
{/* Плейсхолдер */}
|
||||
<Show when={!content()}>
|
||||
<div
|
||||
class={styles.placeholder}
|
||||
onClick={() => setIsEditing(true)}
|
||||
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666; cursor: pointer; font-style: italic; font-size: 14px;"
|
||||
>
|
||||
<div class={styles.placeholder} onClick={() => setIsEditing(true)}>
|
||||
{props.placeholder || 'Нажмите для редактирования...'}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Кнопки управления внизу */}
|
||||
{props.showButtons !== false && (
|
||||
<div class={styles.editorControls} style="border-top: 1px solid rgba(255, 255, 255, 0.1); border-bottom: none; background-color: #1e1e1e;">
|
||||
<Show when={isEditing()} fallback={
|
||||
<Show when={props.showButtons}>
|
||||
<div class={styles.editorControls}>
|
||||
<Show
|
||||
when={isEditing()}
|
||||
fallback={
|
||||
<button class={styles.editButton} onClick={() => setIsEditing(true)}>
|
||||
✏️ Редактировать
|
||||
</button>
|
||||
}>
|
||||
}
|
||||
>
|
||||
<div class={styles.editingControls}>
|
||||
<button class={styles.saveButton} onClick={handleSave}>
|
||||
💾 Сохранить (Ctrl+Enter)
|
||||
|
@ -449,7 +400,7 @@ const EditableCodePreview = (props: EditableCodePreviewProps) => {
|
|||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user