Обновление интерфейса задач и версия 3.21.0
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 49s

This commit is contained in:
poignatov
2026-01-20 20:40:38 +03:00
parent 2369661015
commit 08d86eb5f5
7 changed files with 80 additions and 59 deletions

View File

@@ -1 +1 @@
3.20.4 3.21.0

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "3.20.4", "version": "3.21.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useRef } from 'react' import React, { useState, useEffect, useCallback, useRef } from 'react'
import CurrentWeek from './components/CurrentWeek' import CurrentWeek from './components/CurrentWeek'
import FullStatistics from './components/FullStatistics' import FullStatistics from './components/FullStatistics'
import ProjectPriorityManager from './components/ProjectPriorityManager' import ProjectPriorityManager from './components/ProjectPriorityManager'
@@ -120,6 +120,10 @@ function AppContent() {
const [wordsRefreshTrigger, setWordsRefreshTrigger] = useState(0) const [wordsRefreshTrigger, setWordsRefreshTrigger] = useState(0)
const [wishlistRefreshTrigger, setWishlistRefreshTrigger] = useState(0) const [wishlistRefreshTrigger, setWishlistRefreshTrigger] = useState(0)
// Состояние для сохранения позиции скролла каждого таба
const [scrollPositions, setScrollPositions] = useState({})
// Восстанавливаем последний выбранный таб после перезагрузки // Восстанавливаем последний выбранный таб после перезагрузки
const [isInitialized, setIsInitialized] = useState(false) const [isInitialized, setIsInitialized] = useState(false)
@@ -624,15 +628,24 @@ function AppContent() {
setTabParams({}) setTabParams({})
updateUrl('full', {}, activeTab) updateUrl('full', {}, activeTab)
} else if (tab !== activeTab || tab === 'task-form' || tab === 'wishlist-form') { } else if (tab !== activeTab || tab === 'task-form' || tab === 'wishlist-form') {
// Сохраняем позицию скролла для текущего таба перед переходом
const scrollContainer = document.querySelector('.flex-1.overflow-y-auto')
if (scrollContainer && mainTabs.includes(activeTab)) {
setScrollPositions(prev => ({
...prev,
[activeTab]: scrollContainer.scrollTop
}))
}
// Для task-form и wishlist-form всегда обновляем параметры, даже если это тот же таб // Для task-form и wishlist-form всегда обновляем параметры, даже если это тот же таб
markTabAsLoaded(tab) markTabAsLoaded(tab)
// Определяем, является ли текущий таб глубоким // Определяем, является ли текущий таб глубоким
const isCurrentTabDeep = deepTabs.includes(activeTab) const isCurrentTabDeep = deepTabs.includes(activeTab)
const isNewTabDeep = deepTabs.includes(tab) const isNewTabDeep = deepTabs.includes(tab)
const isCurrentTabMain = mainTabs.includes(activeTab) const isCurrentTabMain = mainTabs.includes(activeTab)
const isNewTabMain = mainTabs.includes(tab) const isNewTabMain = mainTabs.includes(tab)
{ {
// Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров // Для task-form и wishlist-form явно удаляем параметры, только если нет никаких параметров
// task-form может иметь taskId (редактирование), wishlistId (создание из желания), или returnTo (возврат после создания) // task-form может иметь taskId (редактирование), wishlistId (создание из желания), или returnTo (возврат после создания)
@@ -660,7 +673,7 @@ function AppContent() {
} }
} }
} }
setActiveTab(tab) setActiveTab(tab)
if (tab === 'current') { if (tab === 'current') {
setSelectedProject(null) setSelectedProject(null)
@@ -696,19 +709,19 @@ function AppContent() {
// Загружаем данные при открытии таба (когда таб становится активным) // Загружаем данные при открытии таба (когда таб становится активным)
const prevActiveTabRef = useRef(null) const prevActiveTabRef = useRef(null)
const lastLoadedTabRef = useRef(null) // Отслеживаем последний загруженный таб, чтобы избежать двойной загрузки const lastLoadedTabRef = useRef(null) // Отслеживаем последний загруженный таб, чтобы избежать двойной загрузки
useEffect(() => { useEffect(() => {
if (!activeTab || !loadedTabs[activeTab]) return if (!activeTab || !loadedTabs[activeTab]) return
const isFirstLoad = !tabsInitializedRef.current[activeTab] const isFirstLoad = !tabsInitializedRef.current[activeTab]
const isReturningToTab = prevActiveTabRef.current !== null && prevActiveTabRef.current !== activeTab const isReturningToTab = prevActiveTabRef.current !== null && prevActiveTabRef.current !== activeTab
// Проверяем, не загружали ли мы уже этот таб в этом рендере // Проверяем, не загружали ли мы уже этот таб в этом рендере
const tabKey = `${activeTab}-${isFirstLoad ? 'first' : 'return'}` const tabKey = `${activeTab}-${isFirstLoad ? 'first' : 'return'}`
if (lastLoadedTabRef.current === tabKey) { if (lastLoadedTabRef.current === tabKey) {
return // Уже загружали return // Уже загружали
} }
if (isFirstLoad) { if (isFirstLoad) {
// Первая загрузка таба // Первая загрузка таба
lastLoadedTabRef.current = tabKey lastLoadedTabRef.current = tabKey
@@ -718,9 +731,25 @@ function AppContent() {
lastLoadedTabRef.current = tabKey lastLoadedTabRef.current = tabKey
loadTabData(activeTab, true) loadTabData(activeTab, true)
} }
prevActiveTabRef.current = activeTab prevActiveTabRef.current = activeTab
}, [activeTab, loadedTabs, loadTabData]) }, [activeTab, loadedTabs, loadTabData])
// Восстанавливаем позицию скролла при возвращении на таб
useEffect(() => {
if (mainTabs.includes(activeTab) && scrollPositions[activeTab] !== undefined) {
// Используем setTimeout, чтобы DOM успел обновиться
const timeoutId = setTimeout(() => {
const scrollContainer = document.querySelector('.flex-1.overflow-y-auto')
if (scrollContainer) {
scrollContainer.scrollTop = scrollPositions[activeTab]
}
}, 0)
return () => clearTimeout(timeoutId)
}
}, [activeTab, scrollPositions])
// Определяем общее состояние загрузки и ошибок для кнопки Refresh // Определяем общее состояние загрузки и ошибок для кнопки Refresh
const isAnyLoading = currentWeekLoading || fullStatisticsLoading || prioritiesLoading || isRefreshing const isAnyLoading = currentWeekLoading || fullStatisticsLoading || prioritiesLoading || isRefreshing

View File

@@ -1,3 +1,4 @@
import React from 'react'
import ProjectProgressBar from './ProjectProgressBar' import ProjectProgressBar from './ProjectProgressBar'
import LoadingError from './LoadingError' import LoadingError from './LoadingError'
import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils' import { getAllProjectsSorted, getProjectColor } from '../utils/projectUtils'

View File

@@ -177,7 +177,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
// Парсим repetition_period если он есть // Парсим repetition_period если он есть
setRepetitionMode('after') setRepetitionMode('after')
const periodStr = data.task.repetition_period.trim() const periodStr = data.task.repetition_period.trim()
console.log('Parsing repetition_period:', periodStr, 'Full task data:', data.task) // Отладка
// PostgreSQL может возвращать INTERVAL в разных форматах: // PostgreSQL может возвращать INTERVAL в разных форматах:
// - "1 day" / "1 days" / "10 days" // - "1 day" / "1 days" / "10 days"
@@ -194,8 +193,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
const value = parseInt(match[1], 10) const value = parseInt(match[1], 10)
const unit = match[2].toLowerCase() const unit = match[2].toLowerCase()
console.log('Matched value:', value, 'unit:', unit) // Отладка
if (!isNaN(value) && value >= 0) { if (!isNaN(value) && value >= 0) {
// Преобразуем единицы PostgreSQL в наш формат // Преобразуем единицы PostgreSQL в наш формат
if (unit.startsWith('minute')) { if (unit.startsWith('minute')) {
@@ -293,8 +290,6 @@ function TaskForm({ onNavigate, taskId, wishlistId, isTest: isTestFromProps = fa
console.log('Failed to parse repetition_period:', periodStr) // Отладка console.log('Failed to parse repetition_period:', periodStr) // Отладка
setRepetitionPeriodValue('') setRepetitionPeriodValue('')
setRepetitionPeriodType('day') setRepetitionPeriodType('day')
} else {
console.log('Successfully parsed repetition_period - value will be set') // Отладка
} }
} else { } else {
console.log('No repetition_period or repetition_date in task data') // Отладка console.log('No repetition_period or repetition_date in task data') // Отладка

View File

@@ -556,10 +556,43 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
onClick={(e) => handleCheckmarkClick(task, e)} onClick={(e) => handleCheckmarkClick(task, e)}
title={isTest ? 'Запустить тест' : (showDetailOnCheckmark ? 'Открыть детали' : 'Выполнить задачу')} title={isTest ? 'Запустить тест' : (showDetailOnCheckmark ? 'Открыть детали' : 'Выполнить задачу')}
> >
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> {isTest ? (
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="2" fill="none" className="checkmark-circle" /> <svg
<path d="M6 10 L9 13 L14 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="checkmark-check" /> width="20"
</svg> height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
) : isWishlist ? (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 12 20 22 4 22 4 12"></polyline>
<rect x="2" y="7" width="20" height="5"></rect>
<line x1="12" y1="22" x2="12" y2="7"></line>
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path>
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path>
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="2" fill="none" className="checkmark-circle" />
<path d="M6 10 L9 13 L14 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="checkmark-check" />
</svg>
)}
</div> </div>
<div className="task-name-container"> <div className="task-name-container">
<div className="task-name-wrapper"> <div className="task-name-wrapper">
@@ -569,43 +602,6 @@ function TaskList({ onNavigate, data, loading, backgroundLoading, error, onRetry
<span className="task-subtasks-count">(+{task.subtasks_count})</span> <span className="task-subtasks-count">(+{task.subtasks_count})</span>
)} )}
<span className="task-badge-bar"> <span className="task-badge-bar">
{isWishlist && (
<svg
className="task-wishlist-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
title="Связано с желанием"
>
<polyline points="20 12 20 22 4 22 4 12"></polyline>
<rect x="2" y="7" width="20" height="5"></rect>
<line x1="12" y1="22" x2="12" y2="7"></line>
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path>
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path>
</svg>
)}
{isTest && (
<svg
className="task-test-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
title="Тест"
>
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
)}
{hasProgression && ( {hasProgression && (
<svg <svg
className="task-progression-icon" className="task-progression-icon"

View File

@@ -236,13 +236,13 @@ function Wishlist({ onNavigate, refreshTrigger = 0, isActive = false, initialBoa
useEffect(() => { useEffect(() => {
if (!initialFetchDoneRef.current) { if (!initialFetchDoneRef.current) {
initialFetchDoneRef.current = true initialFetchDoneRef.current = true
// Загружаем доски из кэша // Загружаем доски из кэша
const boardsCacheLoaded = loadBoardsFromCache() const boardsCacheLoaded = loadBoardsFromCache()
if (boardsCacheLoaded) { if (boardsCacheLoaded) {
setBoardsLoading(false) setBoardsLoading(false)
} }
// Загружаем доски с сервера // Загружаем доски с сервера
fetchBoards() fetchBoards()
} }