From 339486f5557bcada58f44efd056ca75dc0d4205a Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sun, 22 Feb 2026 22:59:52 -0500 Subject: [PATCH] fix: resolve CI lint errors and httpx dependency conflict - Fix httpx version conflict: requirements-dev.txt now uses >=0.27.0 to match requirements.txt - Extract CSAT helper functions to csatUtils.ts to fix react-refresh/only-export-components - Remove default export from admin/EmptyState.tsx shim (same rule) - Fix empty catch block in Modal.tsx (no-empty) - Add eslint-disable comments for intentional setState-in-effect patterns in FlowAnalyticsPanel, QuickLaunch, NodeEditorPanel, useCachedQuota, MyAnalyticsPage, TeamAnalyticsPage - Add eslint-disable comments for intentional _children destructure in NodeEditorPanel - Fix _parentId unused var in useTreeLayout.ts - Rewrite usePaginationParams.ts to avoid reading refs during render Co-Authored-By: Claude Sonnet 4.6 --- backend/requirements-dev.txt | 2 +- frontend/src/components/admin/EmptyState.tsx | 1 - .../analytics/FlowAnalyticsPanel.tsx | 2 ++ frontend/src/components/common/Modal.tsx | 4 +++- .../src/components/layout/QuickLaunch.tsx | 1 + frontend/src/components/session/CSATModal.tsx | 21 +------------------ frontend/src/components/session/csatUtils.ts | 19 +++++++++++++++++ .../tree-editor/NodeEditorPanel.tsx | 5 +++++ .../components/tree-editor/useTreeLayout.ts | 1 + frontend/src/hooks/useCachedQuota.ts | 2 ++ frontend/src/hooks/usePaginationParams.ts | 12 ++++------- frontend/src/pages/MyAnalyticsPage.tsx | 1 + .../src/pages/ProceduralNavigationPage.tsx | 3 ++- frontend/src/pages/TeamAnalyticsPage.tsx | 1 + frontend/src/pages/TreeNavigationPage.tsx | 3 ++- 15 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 frontend/src/components/session/csatUtils.ts diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index d974d0bd..bce6e475 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -4,7 +4,7 @@ # Testing pytest==7.4.3 pytest-asyncio==0.23.0 -httpx==0.26.0 +httpx>=0.27.0 pytest-cov==4.1.0 # Code quality diff --git a/frontend/src/components/admin/EmptyState.tsx b/frontend/src/components/admin/EmptyState.tsx index 4e58f7a8..1fd1c8a8 100644 --- a/frontend/src/components/admin/EmptyState.tsx +++ b/frontend/src/components/admin/EmptyState.tsx @@ -1,2 +1 @@ export { EmptyState } from '@/components/common/EmptyState' -export { default } from '@/components/common/EmptyState' diff --git a/frontend/src/components/analytics/FlowAnalyticsPanel.tsx b/frontend/src/components/analytics/FlowAnalyticsPanel.tsx index 7c5823ea..64afac14 100644 --- a/frontend/src/components/analytics/FlowAnalyticsPanel.tsx +++ b/frontend/src/components/analytics/FlowAnalyticsPanel.tsx @@ -37,7 +37,9 @@ export function FlowAnalyticsPanel({ treeId }: FlowAnalyticsPanelProps) { const [error, setError] = useState(false) useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true) + // eslint-disable-next-line react-hooks/set-state-in-effect setError(false) analyticsApi .getFlowAnalytics(treeId, period) diff --git a/frontend/src/components/common/Modal.tsx b/frontend/src/components/common/Modal.tsx index f798e4bb..58262b47 100644 --- a/frontend/src/components/common/Modal.tsx +++ b/frontend/src/components/common/Modal.tsx @@ -29,7 +29,9 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md', a setIsFullScreen(next) try { localStorage.setItem('rf-editor-fullscreen', String(next)) - } catch {} + } catch { + // localStorage unavailable — ignore + } } // Close on Escape key diff --git a/frontend/src/components/layout/QuickLaunch.tsx b/frontend/src/components/layout/QuickLaunch.tsx index 76da29fe..11337487 100644 --- a/frontend/src/components/layout/QuickLaunch.tsx +++ b/frontend/src/components/layout/QuickLaunch.tsx @@ -40,6 +40,7 @@ export function QuickLaunch({ open, onClose }: QuickLaunchProps) { useEffect(() => { if (open) { + // eslint-disable-next-line react-hooks/set-state-in-effect setSelectedIndex(0) treesApi.list({ sort_by: 'updated_at' }) .then(trees => setRecentTrees(trees.slice(0, 4))) diff --git a/frontend/src/components/session/CSATModal.tsx b/frontend/src/components/session/CSATModal.tsx index b99bcc74..6611bb6a 100644 --- a/frontend/src/components/session/CSATModal.tsx +++ b/frontend/src/components/session/CSATModal.tsx @@ -3,6 +3,7 @@ import { Star } from 'lucide-react' import { Modal } from '@/components/common/Modal' import { analyticsApi } from '@/api' import { cn } from '@/lib/utils' +import { markSessionRated } from './csatUtils' interface CSATModalProps { isOpen: boolean @@ -10,26 +11,6 @@ interface CSATModalProps { sessionId: string } -const RATED_SESSIONS_KEY = 'rf-rated-sessions' - -function getRatedSessions(): string[] { - try { - return JSON.parse(localStorage.getItem(RATED_SESSIONS_KEY) || '[]') - } catch { - return [] - } -} - -function markSessionRated(sessionId: string) { - const rated = getRatedSessions() - rated.push(sessionId) - localStorage.setItem(RATED_SESSIONS_KEY, JSON.stringify(rated.slice(-100))) -} - -export function hasBeenRated(sessionId: string): boolean { - return getRatedSessions().includes(sessionId) -} - export function CSATModal({ isOpen, onClose, sessionId }: CSATModalProps) { const [rating, setRating] = useState(0) const [hoveredRating, setHoveredRating] = useState(0) diff --git a/frontend/src/components/session/csatUtils.ts b/frontend/src/components/session/csatUtils.ts new file mode 100644 index 00000000..8ffceea5 --- /dev/null +++ b/frontend/src/components/session/csatUtils.ts @@ -0,0 +1,19 @@ +const RATED_SESSIONS_KEY = 'rf-rated-sessions' + +export function getRatedSessions(): string[] { + try { + return JSON.parse(localStorage.getItem(RATED_SESSIONS_KEY) || '[]') + } catch { + return [] + } +} + +export function markSessionRated(sessionId: string) { + const rated = getRatedSessions() + rated.push(sessionId) + localStorage.setItem(RATED_SESSIONS_KEY, JSON.stringify(rated.slice(-100))) +} + +export function hasBeenRated(sessionId: string): boolean { + return getRatedSessions().includes(sessionId) +} diff --git a/frontend/src/components/tree-editor/NodeEditorPanel.tsx b/frontend/src/components/tree-editor/NodeEditorPanel.tsx index a6d76349..c1362b06 100644 --- a/frontend/src/components/tree-editor/NodeEditorPanel.tsx +++ b/frontend/src/components/tree-editor/NodeEditorPanel.tsx @@ -21,6 +21,7 @@ const TYPE_CONFIG: Record, { icon: typeof HelpCircle } function cloneWithoutChildren(node: TreeStructure): TreeStructure { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { children: _children, ...rest } = node return structuredClone(rest) as TreeStructure } @@ -44,8 +45,11 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan // (e.g., answer stub → decision/action/solution via type picker) useEffect(() => { if (node) { + // eslint-disable-next-line react-hooks/set-state-in-effect setDraft(cloneWithoutChildren(node)) + // eslint-disable-next-line react-hooks/set-state-in-effect setIsDirty(false) + // eslint-disable-next-line react-hooks/set-state-in-effect setShowDeleteConfirm(false) } }, [nodeId, node?.type]) // eslint-disable-line react-hooks/exhaustive-deps @@ -57,6 +61,7 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan const handleSave = useCallback(() => { if (!draft || !node) return + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { children: _children, ...draftWithoutChildren } = draft updateNode(nodeId, draftWithoutChildren) diff --git a/frontend/src/components/tree-editor/useTreeLayout.ts b/frontend/src/components/tree-editor/useTreeLayout.ts index d0c146be..c600c70e 100644 --- a/frontend/src/components/tree-editor/useTreeLayout.ts +++ b/frontend/src/components/tree-editor/useTreeLayout.ts @@ -60,6 +60,7 @@ export function useTreeLayout(): UseTreeLayoutResult { if (!treeStructure) return { rawNodes: nodes, rawEdges: edges } + // eslint-disable-next-line @typescript-eslint/no-unused-vars function walk(node: TreeStructure, _parentId?: string | null) { const isCollapsed = collapsedNodeIds.has(node.id) const hasChildren = (node.children?.length ?? 0) > 0 diff --git a/frontend/src/hooks/useCachedQuota.ts b/frontend/src/hooks/useCachedQuota.ts index 32f3175f..28a41f00 100644 --- a/frontend/src/hooks/useCachedQuota.ts +++ b/frontend/src/hooks/useCachedQuota.ts @@ -16,7 +16,9 @@ export function useCachedQuota() { useEffect(() => { if (cachedResult && Date.now() - cachedResult.timestamp < CACHE_TTL_MS) { + // eslint-disable-next-line react-hooks/set-state-in-effect setAiEnabled(cachedResult.aiEnabled) + // eslint-disable-next-line react-hooks/set-state-in-effect setIsLoading(false) return } diff --git a/frontend/src/hooks/usePaginationParams.ts b/frontend/src/hooks/usePaginationParams.ts index 3a643bac..f4c02a67 100644 --- a/frontend/src/hooks/usePaginationParams.ts +++ b/frontend/src/hooks/usePaginationParams.ts @@ -1,5 +1,5 @@ import { useSearchParams } from 'react-router-dom' -import { useCallback, useMemo, useRef } from 'react' +import { useCallback, useMemo } from 'react' type PageSize = number | 'all' @@ -13,10 +13,6 @@ const DEFAULT_ALLOWED: PageSize[] = [10, 25, 50, 'all'] export function usePaginationParams(options: UsePaginationParamsOptions = {}) { const { defaultPageSize = 10, allowedPageSizes = DEFAULT_ALLOWED } = options - // Stabilize the array reference to avoid re-render loops - const allowedRef = useRef(allowedPageSizes) - allowedRef.current = allowedPageSizes - const [searchParams, setSearchParams] = useSearchParams() const page = useMemo(() => { @@ -27,11 +23,11 @@ export function usePaginationParams(options: UsePaginationParamsOptions = {}) { const pageSize = useMemo((): PageSize => { const raw = searchParams.get('size') - if (raw === 'all' && allowedRef.current.includes('all')) return 'all' + if (raw === 'all' && allowedPageSizes.includes('all')) return 'all' const n = raw ? parseInt(raw, 10) : defaultPageSize - if (Number.isFinite(n) && allowedRef.current.includes(n)) return n + if (Number.isFinite(n) && allowedPageSizes.includes(n)) return n return defaultPageSize - }, [searchParams, defaultPageSize]) + }, [searchParams, defaultPageSize, allowedPageSizes]) const setPage = useCallback( (newPage: number) => { diff --git a/frontend/src/pages/MyAnalyticsPage.tsx b/frontend/src/pages/MyAnalyticsPage.tsx index a7b7d88a..e5aa3564 100644 --- a/frontend/src/pages/MyAnalyticsPage.tsx +++ b/frontend/src/pages/MyAnalyticsPage.tsx @@ -35,6 +35,7 @@ export default function MyAnalyticsPage() { const [loading, setLoading] = useState(true) useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true) analyticsApi .getPersonalAnalytics(period) diff --git a/frontend/src/pages/ProceduralNavigationPage.tsx b/frontend/src/pages/ProceduralNavigationPage.tsx index 4d313ace..8f416d2a 100644 --- a/frontend/src/pages/ProceduralNavigationPage.tsx +++ b/frontend/src/pages/ProceduralNavigationPage.tsx @@ -14,7 +14,8 @@ import { Spinner } from '@/components/common/Spinner' import { cn } from '@/lib/utils' import { toast } from '@/lib/toast' import { StepFeedback } from '@/components/session/StepFeedback' -import { CSATModal, hasBeenRated } from '@/components/session/CSATModal' +import { CSATModal } from '@/components/session/CSATModal' +import { hasBeenRated } from '@/components/session/csatUtils' interface StepState { notes: string diff --git a/frontend/src/pages/TeamAnalyticsPage.tsx b/frontend/src/pages/TeamAnalyticsPage.tsx index f2a27342..950533aa 100644 --- a/frontend/src/pages/TeamAnalyticsPage.tsx +++ b/frontend/src/pages/TeamAnalyticsPage.tsx @@ -37,6 +37,7 @@ export default function TeamAnalyticsPage() { useEffect(() => { if (!isAccountOwner && !isSuperAdmin) return + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true) analyticsApi .getTeamAnalytics(period) diff --git a/frontend/src/pages/TreeNavigationPage.tsx b/frontend/src/pages/TreeNavigationPage.tsx index 00f31bf8..848ae3af 100644 --- a/frontend/src/pages/TreeNavigationPage.tsx +++ b/frontend/src/pages/TreeNavigationPage.tsx @@ -15,7 +15,8 @@ import { Spinner } from '@/components/common/Spinner' import { toast } from '@/lib/toast' import { Modal } from '@/components/common/Modal' import { ShareSessionModal } from '@/components/session/ShareSessionModal' -import { CSATModal, hasBeenRated } from '@/components/session/CSATModal' +import { CSATModal } from '@/components/session/CSATModal' +import { hasBeenRated } from '@/components/session/csatUtils' import { StepFeedback } from '@/components/session/StepFeedback' import { buildSessionShareUrl, getLatestActiveShareForSession } from '@/lib/sessionShare'