Оптимизация wishlist: раздельные запросы и копирование
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m14s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m14s
This commit is contained in:
@@ -7,8 +7,9 @@ import './WishlistForm.css'
|
||||
const API_URL = '/api/wishlist'
|
||||
const TASKS_API_URL = '/api/tasks'
|
||||
const PROJECTS_API_URL = '/projects'
|
||||
const WISHLIST_FORM_STATE_KEY = 'wishlistFormPendingState'
|
||||
|
||||
function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) {
|
||||
function WishlistForm({ onNavigate, wishlistId, editConditionIndex, newTaskId }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [name, setName] = useState('')
|
||||
const [price, setPrice] = useState('')
|
||||
@@ -29,6 +30,7 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) {
|
||||
const [toastMessage, setToastMessage] = useState(null)
|
||||
const [loadingWishlist, setLoadingWishlist] = useState(false)
|
||||
const [fetchingMetadata, setFetchingMetadata] = useState(false)
|
||||
const [restoredFromSession, setRestoredFromSession] = useState(false) // Флаг восстановления из sessionStorage
|
||||
const fileInputRef = useRef(null)
|
||||
|
||||
// Загрузка задач и проектов
|
||||
@@ -57,13 +59,19 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) {
|
||||
|
||||
// Загрузка желания при редактировании или сброс формы при создании
|
||||
useEffect(() => {
|
||||
// Пропускаем загрузку, если состояние было восстановлено из sessionStorage
|
||||
if (restoredFromSession) {
|
||||
console.log('[WishlistForm] Skipping loadWishlist - restored from session')
|
||||
return
|
||||
}
|
||||
|
||||
if (wishlistId !== undefined && wishlistId !== null && tasks.length > 0 && projects.length > 0) {
|
||||
loadWishlist()
|
||||
} else if (wishlistId === undefined || wishlistId === null) {
|
||||
// Сбрасываем форму при создании новой задачи
|
||||
resetForm()
|
||||
}
|
||||
}, [wishlistId, tasks, projects])
|
||||
}, [wishlistId, tasks, projects, restoredFromSession])
|
||||
|
||||
// Сброс формы при размонтировании компонента или при изменении wishlistId на undefined
|
||||
useEffect(() => {
|
||||
@@ -84,6 +92,89 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) {
|
||||
}
|
||||
}, [editConditionIndex, unlockConditions])
|
||||
|
||||
// Восстановление состояния при возврате с создания задачи
|
||||
useEffect(() => {
|
||||
const savedState = sessionStorage.getItem(WISHLIST_FORM_STATE_KEY)
|
||||
console.log('[WishlistForm] Checking restore - newTaskId:', newTaskId, 'savedState exists:', !!savedState)
|
||||
|
||||
if (savedState && newTaskId) {
|
||||
console.log('[WishlistForm] Starting restoration...')
|
||||
try {
|
||||
const state = JSON.parse(savedState)
|
||||
console.log('[WishlistForm] Parsed state:', state)
|
||||
|
||||
// Восстанавливаем состояние формы
|
||||
setName(state.name || '')
|
||||
setPrice(state.price || '')
|
||||
setLink(state.link || '')
|
||||
setImageUrl(state.imageUrl || null)
|
||||
|
||||
// Восстанавливаем условия и автоматически добавляем новую задачу
|
||||
const restoredConditions = state.unlockConditions || []
|
||||
console.log('[WishlistForm] Restored conditions:', restoredConditions)
|
||||
|
||||
// Перезагружаем задачи, чтобы новая задача была в списке
|
||||
const reloadTasks = async () => {
|
||||
console.log('[WishlistForm] Reloading tasks...')
|
||||
try {
|
||||
const tasksResponse = await authFetch(TASKS_API_URL)
|
||||
console.log('[WishlistForm] Tasks response ok:', tasksResponse.ok)
|
||||
if (tasksResponse.ok) {
|
||||
const tasksData = await tasksResponse.json()
|
||||
console.log('[WishlistForm] Tasks loaded:', tasksData.length)
|
||||
setTasks(Array.isArray(tasksData) ? tasksData : [])
|
||||
|
||||
// Автоматически добавляем цель с новой задачей
|
||||
console.log('[WishlistForm] pendingConditionType:', state.pendingConditionType)
|
||||
if (state.pendingConditionType === 'task_completion') {
|
||||
const newCondition = {
|
||||
type: 'task_completion',
|
||||
task_id: newTaskId,
|
||||
project_id: null,
|
||||
required_points: null,
|
||||
start_date: null,
|
||||
display_order: restoredConditions.length,
|
||||
}
|
||||
console.log('[WishlistForm] New condition to add:', newCondition)
|
||||
|
||||
// Если редактировали существующее условие, заменяем его
|
||||
if (state.editingConditionIndex !== null && state.editingConditionIndex !== undefined) {
|
||||
console.log('[WishlistForm] Replacing existing condition at index:', state.editingConditionIndex)
|
||||
const updatedConditions = restoredConditions.map((cond, idx) =>
|
||||
idx === state.editingConditionIndex ? { ...newCondition, display_order: idx } : cond
|
||||
)
|
||||
setUnlockConditions(updatedConditions)
|
||||
console.log('[WishlistForm] Updated conditions:', updatedConditions)
|
||||
} else {
|
||||
// Добавляем новое условие
|
||||
const finalConditions = [...restoredConditions, newCondition]
|
||||
console.log('[WishlistForm] Adding new condition, final conditions:', finalConditions)
|
||||
setUnlockConditions(finalConditions)
|
||||
}
|
||||
} else {
|
||||
setUnlockConditions(restoredConditions)
|
||||
}
|
||||
|
||||
// Устанавливаем флаг, что состояние восстановлено
|
||||
setRestoredFromSession(true)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[WishlistForm] Error reloading tasks:', err)
|
||||
setUnlockConditions(restoredConditions)
|
||||
}
|
||||
}
|
||||
reloadTasks()
|
||||
|
||||
// Очищаем sessionStorage
|
||||
sessionStorage.removeItem(WISHLIST_FORM_STATE_KEY)
|
||||
console.log('[WishlistForm] SessionStorage cleared')
|
||||
} catch (e) {
|
||||
console.error('[WishlistForm] Error restoring wishlist form state:', e)
|
||||
sessionStorage.removeItem(WISHLIST_FORM_STATE_KEY)
|
||||
}
|
||||
}
|
||||
}, [newTaskId, authFetch])
|
||||
|
||||
const loadWishlist = async () => {
|
||||
setLoadingWishlist(true)
|
||||
try {
|
||||
@@ -356,6 +447,30 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) {
|
||||
setUnlockConditions(unlockConditions.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
// Обработчик для создания задачи из ConditionForm
|
||||
const handleCreateTaskFromCondition = () => {
|
||||
// Сохранить текущее состояние формы
|
||||
const stateToSave = {
|
||||
name,
|
||||
price,
|
||||
link,
|
||||
imageUrl,
|
||||
unlockConditions,
|
||||
pendingConditionType: 'task_completion',
|
||||
editingConditionIndex,
|
||||
}
|
||||
console.log('[WishlistForm] Saving state and navigating to task-form:', stateToSave)
|
||||
sessionStorage.setItem(WISHLIST_FORM_STATE_KEY, JSON.stringify(stateToSave))
|
||||
|
||||
// Навигация на форму создания задачи
|
||||
const navParams = {
|
||||
returnTo: 'wishlist-form',
|
||||
returnWishlistId: wishlistId,
|
||||
}
|
||||
console.log('[WishlistForm] Navigation params:', navParams)
|
||||
onNavigate?.('task-form', navParams)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
@@ -640,6 +755,8 @@ function WishlistForm({ onNavigate, wishlistId, editConditionIndex }) {
|
||||
onSubmit={handleConditionSubmit}
|
||||
onCancel={handleConditionCancel}
|
||||
editingCondition={editingConditionIndex !== null ? unlockConditions[editingConditionIndex] : null}
|
||||
onCreateTask={handleCreateTaskFromCondition}
|
||||
preselectedTaskId={newTaskId}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -732,22 +849,211 @@ function DateSelector({ value, onChange, placeholder = "За всё время"
|
||||
)
|
||||
}
|
||||
|
||||
// Компонент автодополнения для выбора задачи
|
||||
function TaskAutocomplete({ tasks, value, onChange, onCreateTask, preselectedTaskId }) {
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
||||
const wrapperRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
// Найти выбранную задачу по ID
|
||||
const selectedTask = tasks.find(t => t.id === value)
|
||||
|
||||
// При изменении selectedTask или value - обновить inputValue
|
||||
useEffect(() => {
|
||||
if (selectedTask) {
|
||||
setInputValue(selectedTask.name)
|
||||
} else if (!value) {
|
||||
setInputValue('')
|
||||
}
|
||||
}, [selectedTask, value])
|
||||
|
||||
// При preselectedTaskId автоматически выбрать задачу
|
||||
useEffect(() => {
|
||||
if (preselectedTaskId && !value && tasks.length > 0) {
|
||||
const task = tasks.find(t => t.id === preselectedTaskId)
|
||||
if (task && value !== preselectedTaskId) {
|
||||
onChange(preselectedTaskId)
|
||||
setInputValue(task.name)
|
||||
}
|
||||
}
|
||||
}, [preselectedTaskId, tasks.length, value, onChange])
|
||||
|
||||
// Фильтрация задач
|
||||
const filteredTasks = inputValue.trim()
|
||||
? tasks.filter(task =>
|
||||
task.name.toLowerCase().includes(inputValue.toLowerCase())
|
||||
)
|
||||
: tasks
|
||||
|
||||
// Закрытие при клике снаружи
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
|
||||
setIsOpen(false)
|
||||
// Восстанавливаем название выбранной задачи
|
||||
if (selectedTask) {
|
||||
setInputValue(selectedTask.name)
|
||||
} else if (!value) {
|
||||
setInputValue('')
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [selectedTask, value])
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
setInputValue(e.target.value)
|
||||
setIsOpen(true)
|
||||
setHighlightedIndex(-1)
|
||||
// Сбрасываем выбор, если пользователь изменил текст
|
||||
if (selectedTask && e.target.value !== selectedTask.name) {
|
||||
onChange(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectTask = (task) => {
|
||||
onChange(task.id)
|
||||
setInputValue(task.name)
|
||||
setIsOpen(false)
|
||||
setHighlightedIndex(-1)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (!isOpen) {
|
||||
if (e.key === 'ArrowDown' || e.key === 'Enter') {
|
||||
setIsOpen(true)
|
||||
e.preventDefault()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex(prev =>
|
||||
prev < filteredTasks.length - 1 ? prev + 1 : prev
|
||||
)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
setHighlightedIndex(prev => prev > 0 ? prev - 1 : -1)
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
if (highlightedIndex >= 0 && filteredTasks[highlightedIndex]) {
|
||||
handleSelectTask(filteredTasks[highlightedIndex])
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
setIsOpen(false)
|
||||
if (selectedTask) {
|
||||
setInputValue(selectedTask.name)
|
||||
} else {
|
||||
setInputValue('')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-autocomplete" ref={wrapperRef}>
|
||||
<div className="task-autocomplete-row">
|
||||
<div className="task-autocomplete-input-wrapper">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onFocus={handleFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Начните вводить название..."
|
||||
className="task-autocomplete-input"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{inputValue && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setInputValue('')
|
||||
onChange(null)
|
||||
inputRef.current?.focus()
|
||||
}}
|
||||
className="task-autocomplete-clear"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCreateTask}
|
||||
className="create-task-button"
|
||||
title="Создать новую задачу"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="task-autocomplete-dropdown">
|
||||
{filteredTasks.length === 0 ? (
|
||||
<div className="task-autocomplete-empty">
|
||||
{inputValue ? 'Задачи не найдены' : 'Нет доступных задач'}
|
||||
</div>
|
||||
) : (
|
||||
filteredTasks.map((task, index) => (
|
||||
<div
|
||||
key={task.id}
|
||||
className={`task-autocomplete-item ${
|
||||
value === task.id ? 'selected' : ''
|
||||
} ${highlightedIndex === index ? 'highlighted' : ''}`}
|
||||
onClick={() => handleSelectTask(task)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
>
|
||||
{task.name}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Компонент формы цели
|
||||
function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition }) {
|
||||
function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition, onCreateTask, preselectedTaskId }) {
|
||||
const [type, setType] = useState(editingCondition?.type || 'project_points')
|
||||
const [taskId, setTaskId] = useState(editingCondition?.task_id?.toString() || '')
|
||||
const [taskId, setTaskId] = useState(editingCondition?.task_id || null)
|
||||
const [projectId, setProjectId] = useState(editingCondition?.project_id?.toString() || '')
|
||||
const [requiredPoints, setRequiredPoints] = useState(editingCondition?.required_points?.toString() || '')
|
||||
const [startDate, setStartDate] = useState(editingCondition?.start_date || '')
|
||||
|
||||
const isEditing = editingCondition !== null
|
||||
|
||||
// Автоподстановка новой задачи
|
||||
useEffect(() => {
|
||||
if (preselectedTaskId && !editingCondition) {
|
||||
setType('task_completion')
|
||||
setTaskId(preselectedTaskId)
|
||||
}
|
||||
}, [preselectedTaskId, editingCondition])
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation() // Предотвращаем всплытие события
|
||||
|
||||
// Валидация
|
||||
if (type === 'task_completion' && !taskId) {
|
||||
if (type === 'task_completion' && (!taskId || taskId === null)) {
|
||||
return
|
||||
}
|
||||
if (type === 'project_points' && (!projectId || !requiredPoints)) {
|
||||
@@ -756,7 +1062,7 @@ function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition }
|
||||
|
||||
const condition = {
|
||||
type,
|
||||
task_id: type === 'task_completion' ? parseInt(taskId) : null,
|
||||
task_id: type === 'task_completion' ? (typeof taskId === 'number' ? taskId : parseInt(taskId)) : null,
|
||||
project_id: type === 'project_points' ? parseInt(projectId) : null,
|
||||
required_points: type === 'project_points' ? parseFloat(requiredPoints) : null,
|
||||
start_date: type === 'project_points' && startDate ? startDate : null,
|
||||
@@ -764,7 +1070,7 @@ function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition }
|
||||
onSubmit(condition)
|
||||
// Сброс формы
|
||||
setType('project_points')
|
||||
setTaskId('')
|
||||
setTaskId(null)
|
||||
setProjectId('')
|
||||
setRequiredPoints('')
|
||||
setStartDate('')
|
||||
@@ -790,17 +1096,13 @@ function ConditionForm({ tasks, projects, onSubmit, onCancel, editingCondition }
|
||||
{type === 'task_completion' && (
|
||||
<div className="form-group">
|
||||
<label>Задача</label>
|
||||
<select
|
||||
<TaskAutocomplete
|
||||
tasks={tasks}
|
||||
value={taskId}
|
||||
onChange={(e) => setTaskId(e.target.value)}
|
||||
className="form-input"
|
||||
required
|
||||
>
|
||||
<option value="">Выберите задачу</option>
|
||||
{tasks.map(task => (
|
||||
<option key={task.id} value={task.id}>{task.name}</option>
|
||||
))}
|
||||
</select>
|
||||
onChange={(id) => setTaskId(id)}
|
||||
onCreateTask={onCreateTask}
|
||||
preselectedTaskId={preselectedTaskId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user