From c10def601b67d9c8b455fbf6bf1131196444ee5a Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sun, 10 May 2026 12:52:36 +0200 Subject: [PATCH] feat(PBI-76): migrate sprint-backlog to user-settings store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces six useState+useEffect+localStorage flows with selectors from useUserSettingsStore. Defaults are applied at the selector level (filterStatus 'OPEN', sort 'code', etc) so the component matches its previous behaviour. The collapsed Set is derived from the persisted array, falling back to auto-collapse-DONE when no preference exists yet. setPref calls are fire-and-forget — the optimistic flow handles the local state update. Co-Authored-By: Claude Opus 4.7 (1M context) --- components/sprint/sprint-backlog.tsx | 105 +++++++++------------------ 1 file changed, 36 insertions(+), 69 deletions(-) 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 = (