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 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
# Testing
|
# Testing
|
||||||
pytest==7.4.3
|
pytest==7.4.3
|
||||||
pytest-asyncio==0.23.0
|
pytest-asyncio==0.23.0
|
||||||
httpx==0.26.0
|
httpx>=0.27.0
|
||||||
pytest-cov==4.1.0
|
pytest-cov==4.1.0
|
||||||
|
|
||||||
# Code quality
|
# Code quality
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export { EmptyState } from '@/components/common/EmptyState'
|
export { EmptyState } from '@/components/common/EmptyState'
|
||||||
export { default } from '@/components/common/EmptyState'
|
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ export function FlowAnalyticsPanel({ treeId }: FlowAnalyticsPanelProps) {
|
|||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setError(false)
|
setError(false)
|
||||||
analyticsApi
|
analyticsApi
|
||||||
.getFlowAnalytics(treeId, period)
|
.getFlowAnalytics(treeId, period)
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md', a
|
|||||||
setIsFullScreen(next)
|
setIsFullScreen(next)
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('rf-editor-fullscreen', String(next))
|
localStorage.setItem('rf-editor-fullscreen', String(next))
|
||||||
} catch {}
|
} catch {
|
||||||
|
// localStorage unavailable — ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close on Escape key
|
// Close on Escape key
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function QuickLaunch({ open, onClose }: QuickLaunchProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setSelectedIndex(0)
|
setSelectedIndex(0)
|
||||||
treesApi.list({ sort_by: 'updated_at' })
|
treesApi.list({ sort_by: 'updated_at' })
|
||||||
.then(trees => setRecentTrees(trees.slice(0, 4)))
|
.then(trees => setRecentTrees(trees.slice(0, 4)))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Star } from 'lucide-react'
|
|||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { analyticsApi } from '@/api'
|
import { analyticsApi } from '@/api'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { markSessionRated } from './csatUtils'
|
||||||
|
|
||||||
interface CSATModalProps {
|
interface CSATModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -10,26 +11,6 @@ interface CSATModalProps {
|
|||||||
sessionId: string
|
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) {
|
export function CSATModal({ isOpen, onClose, sessionId }: CSATModalProps) {
|
||||||
const [rating, setRating] = useState(0)
|
const [rating, setRating] = useState(0)
|
||||||
const [hoveredRating, setHoveredRating] = useState(0)
|
const [hoveredRating, setHoveredRating] = useState(0)
|
||||||
|
|||||||
19
frontend/src/components/session/csatUtils.ts
Normal file
19
frontend/src/components/session/csatUtils.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ const TYPE_CONFIG: Record<Exclude<NodeType, 'answer'>, { icon: typeof HelpCircle
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cloneWithoutChildren(node: TreeStructure): TreeStructure {
|
function cloneWithoutChildren(node: TreeStructure): TreeStructure {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { children: _children, ...rest } = node
|
const { children: _children, ...rest } = node
|
||||||
return structuredClone(rest) as TreeStructure
|
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)
|
// (e.g., answer stub → decision/action/solution via type picker)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (node) {
|
if (node) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setDraft(cloneWithoutChildren(node))
|
setDraft(cloneWithoutChildren(node))
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setIsDirty(false)
|
setIsDirty(false)
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setShowDeleteConfirm(false)
|
setShowDeleteConfirm(false)
|
||||||
}
|
}
|
||||||
}, [nodeId, node?.type]) // eslint-disable-line react-hooks/exhaustive-deps
|
}, [nodeId, node?.type]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
@@ -57,6 +61,7 @@ export function NodeEditorPanel({ nodeId, onClose, onSelectType }: NodeEditorPan
|
|||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
if (!draft || !node) return
|
if (!draft || !node) return
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { children: _children, ...draftWithoutChildren } = draft
|
const { children: _children, ...draftWithoutChildren } = draft
|
||||||
updateNode(nodeId, draftWithoutChildren)
|
updateNode(nodeId, draftWithoutChildren)
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export function useTreeLayout(): UseTreeLayoutResult {
|
|||||||
|
|
||||||
if (!treeStructure) return { rawNodes: nodes, rawEdges: edges }
|
if (!treeStructure) return { rawNodes: nodes, rawEdges: edges }
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
function walk(node: TreeStructure, _parentId?: string | null) {
|
function walk(node: TreeStructure, _parentId?: string | null) {
|
||||||
const isCollapsed = collapsedNodeIds.has(node.id)
|
const isCollapsed = collapsedNodeIds.has(node.id)
|
||||||
const hasChildren = (node.children?.length ?? 0) > 0
|
const hasChildren = (node.children?.length ?? 0) > 0
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export function useCachedQuota() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cachedResult && Date.now() - cachedResult.timestamp < CACHE_TTL_MS) {
|
if (cachedResult && Date.now() - cachedResult.timestamp < CACHE_TTL_MS) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setAiEnabled(cachedResult.aiEnabled)
|
setAiEnabled(cachedResult.aiEnabled)
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import { useCallback, useMemo, useRef } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
type PageSize = number | 'all'
|
type PageSize = number | 'all'
|
||||||
|
|
||||||
@@ -13,10 +13,6 @@ const DEFAULT_ALLOWED: PageSize[] = [10, 25, 50, 'all']
|
|||||||
export function usePaginationParams(options: UsePaginationParamsOptions = {}) {
|
export function usePaginationParams(options: UsePaginationParamsOptions = {}) {
|
||||||
const { defaultPageSize = 10, allowedPageSizes = DEFAULT_ALLOWED } = options
|
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 [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
const page = useMemo(() => {
|
const page = useMemo(() => {
|
||||||
@@ -27,11 +23,11 @@ export function usePaginationParams(options: UsePaginationParamsOptions = {}) {
|
|||||||
|
|
||||||
const pageSize = useMemo((): PageSize => {
|
const pageSize = useMemo((): PageSize => {
|
||||||
const raw = searchParams.get('size')
|
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
|
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
|
return defaultPageSize
|
||||||
}, [searchParams, defaultPageSize])
|
}, [searchParams, defaultPageSize, allowedPageSizes])
|
||||||
|
|
||||||
const setPage = useCallback(
|
const setPage = useCallback(
|
||||||
(newPage: number) => {
|
(newPage: number) => {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export default function MyAnalyticsPage() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
analyticsApi
|
analyticsApi
|
||||||
.getPersonalAnalytics(period)
|
.getPersonalAnalytics(period)
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { Spinner } from '@/components/common/Spinner'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { StepFeedback } from '@/components/session/StepFeedback'
|
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 {
|
interface StepState {
|
||||||
notes: string
|
notes: string
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default function TeamAnalyticsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAccountOwner && !isSuperAdmin) return
|
if (!isAccountOwner && !isSuperAdmin) return
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
analyticsApi
|
analyticsApi
|
||||||
.getTeamAnalytics(period)
|
.getTeamAnalytics(period)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { Spinner } from '@/components/common/Spinner'
|
|||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
import { Modal } from '@/components/common/Modal'
|
import { Modal } from '@/components/common/Modal'
|
||||||
import { ShareSessionModal } from '@/components/session/ShareSessionModal'
|
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 { StepFeedback } from '@/components/session/StepFeedback'
|
||||||
import { buildSessionShareUrl, getLatestActiveShareForSession } from '@/lib/sessionShare'
|
import { buildSessionShareUrl, getLatestActiveShareForSession } from '@/lib/sessionShare'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user