diff --git a/components/sprint/sprint-backlog.tsx b/components/sprint/sprint-backlog.tsx index f231344..9793a17 100644 --- a/components/sprint/sprint-backlog.tsx +++ b/components/sprint/sprint-backlog.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useTransition, useEffect } from 'react' +import { useMemo, useState, useTransition } from 'react' import { Trash2, MoreHorizontal, ChevronsUp, ChevronsDown, Pencil } from 'lucide-react' import { useDroppable, useDraggable } from '@dnd-kit/core' import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable' @@ -9,7 +9,7 @@ import { toast } from 'sonner' import { useShallow } from 'zustand/react/shallow' import { Badge } from '@/components/ui/badge' import { CodeBadge } from '@/components/shared/code-badge' -import { readLocalStoragePref } from '@/lib/use-local-storage-pref' +import { useUserSettingsStore } from '@/stores/user-settings/store' import { BacklogFilterPopover, PRIORITY_LABELS as SHARED_PRIORITY_LABELS, @@ -453,75 +453,43 @@ interface SprintBacklogRightProps { } export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, productId, onAdd }: SprintBacklogRightProps) { - const [collapsed, setCollapsed] = useState>(() => { + const prefs = useUserSettingsStore( + useShallow((s) => s.entities.settings.views?.sprintBacklog ?? {}), + ) + const setPref = useUserSettingsStore((s) => s.setPref) + + const filterPriority = prefs.filterPriority ?? 'all' + const filterStatus: StoryStatusFilter = prefs.filterStatus ?? 'OPEN' + const sort: PbiSort = prefs.sort ?? 'code' + const sortDir: SortDir = prefs.sortDir ?? 'asc' + const filterPopoverOpen = prefs.filterPopoverOpen ?? false + + const collapsed = useMemo>(() => { + if (prefs.collapsedPbis !== undefined) return new Set(prefs.collapsedPbis) + // Default: auto-collapse PBIs whose stories are all DONE. const auto = new Set() for (const pbi of pbisWithStories) { - if (pbi.stories.length > 0 && pbi.stories.every(s => s.status === 'DONE')) { + if (pbi.stories.length > 0 && pbi.stories.every((s) => s.status === 'DONE')) { auto.add(pbi.id) } } return auto - }) - const [filterPriority, setFilterPriority] = useState('all') - const [filterStatus, setFilterStatus] = useState('OPEN') - const [sort, setSort] = useState('code') - const [sortDir, setSortDir] = useState('asc') - const [filterPopoverOpen, setFilterPopoverOpen] = useState(false) - const [prefsLoaded, setPrefsLoaded] = useState(false) + }, [prefs.collapsedPbis, pbisWithStories]) + + const setFilterPriority = (v: number | 'all') => + void setPref(['views', 'sprintBacklog', 'filterPriority'], v) + const setFilterStatus = (v: StoryStatusFilter) => + void setPref(['views', 'sprintBacklog', 'filterStatus'], v) + const setSort = (v: PbiSort) => void setPref(['views', 'sprintBacklog', 'sort'], v) + const setSortDir = (v: SortDir) => void setPref(['views', 'sprintBacklog', 'sortDir'], v) + const setFilterPopoverOpen = (v: boolean) => + void setPref(['views', 'sprintBacklog', 'filterPopoverOpen'], v) + const setCollapsedArray = (next: Set) => + void setPref(['views', 'sprintBacklog', 'collapsedPbis'], Array.from(next)) + const [pbiDialogState, setPbiDialogState] = useState(null) const { setNodeRef, isOver } = useDroppable({ id: 'backlog-zone' }) - // Hydrate prefs from localStorage post-mount. SSR & first client render use - // defaults — matched HTML so no hydration error. After mount we apply saved - // values; users with saved == default see no visible change, others see one - // filter update during hydration. - useEffect(() => { - /* eslint-disable react-hooks/set-state-in-effect */ - setFilterPriority(readLocalStoragePref( - 'scrum4me:sprint_pb_filter_priority', - (raw) => { - if (raw === 'all') return 'all' - const n = parseInt(raw, 10) - return Number.isInteger(n) && n >= 1 && n <= 4 ? n : null - }, - 'all', - )) - setFilterStatus(readLocalStoragePref( - 'scrum4me:sprint_pb_filter_status', - (raw) => (raw === 'OPEN' || raw === 'IN_SPRINT' || raw === 'DONE' || raw === 'all') ? raw : null, - 'OPEN', - )) - setSort(readLocalStoragePref( - 'scrum4me:sprint_pb_sort', - (raw) => (raw === 'priority' || raw === 'status' || raw === 'code') ? raw : null, - 'code', - )) - setSortDir(readLocalStoragePref( - 'scrum4me:sprint_pb_sort_dir', - (raw) => (raw === 'asc' || raw === 'desc') ? raw : null, - 'asc', - )) - const savedCollapsed = localStorage.getItem('scrum4me:sprint_pb_collapsed') - if (savedCollapsed) { - try { - const arr = JSON.parse(savedCollapsed) - if (Array.isArray(arr)) { - setCollapsed(new Set(arr.filter((x): x is string => typeof x === 'string'))) - } - } catch { /* ignore malformed JSON */ } - } - setFilterPopoverOpen(localStorage.getItem('scrum4me:sprint_pb_filter_popover_open') === 'true') - setPrefsLoaded(true) - /* eslint-enable react-hooks/set-state-in-effect */ - }, []) - - useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:sprint_pb_filter_priority', String(filterPriority)) }, [filterPriority, prefsLoaded]) - useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:sprint_pb_filter_status', filterStatus) }, [filterStatus, prefsLoaded]) - useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:sprint_pb_sort', sort) }, [sort, prefsLoaded]) - useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:sprint_pb_sort_dir', sortDir) }, [sortDir, prefsLoaded]) - useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:sprint_pb_collapsed', JSON.stringify(Array.from(collapsed))) }, [collapsed, prefsLoaded]) - useEffect(() => { if (prefsLoaded) localStorage.setItem('scrum4me:sprint_pb_filter_popover_open', String(filterPopoverOpen)) }, [filterPopoverOpen, prefsLoaded]) - const filteredPbis = pbisWithStories .map(pbi => ({ ...pbi, @@ -538,19 +506,18 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, pr (filterStatus !== 'OPEN' ? 1 : 0) function toggle(pbiId: string) { - setCollapsed(prev => { - const next = new Set(prev) - if (next.has(pbiId)) { next.delete(pbiId) } else { next.add(pbiId) } - return next - }) + const next = new Set(collapsed) + if (next.has(pbiId)) next.delete(pbiId) + else next.add(pbiId) + setCollapsedArray(next) } function collapseAll() { - setCollapsed(new Set(filteredPbis.map(p => p.id))) + setCollapsedArray(new Set(filteredPbis.map((p) => p.id))) } function expandAll() { - setCollapsed(new Set()) + setCollapsedArray(new Set()) } const headerActions = (