Skip to content

Commit e19f391

Browse files
OrssoAdrien Reibel
andauthored
fix: repair frontend ESLint errors and backend CI pipeline (#1)
* fix: resolve all frontend ESLint errors and warnings - question-input: stop reading submittedRef during render (react-hooks/refs) - dashboard-grid: move ref assignments into useEffect (react-hooks/refs) - use-upload-message: use lazy initializer instead of setState in effect - card-editor: fix useEffect dependency array, remove stale eslint-disable - portaled-components: remove unused eslint-disable comment - ci: add explicit python-version to uv setup steps * fix: revert ci.yml python-version change that breaks backend lint The python-version parameter in setup-uv causes ruff to not be found. Revert to match the working main branch configuration. * fix: use dependency groups and pin Python 3.13 for reliable CI - Add .python-version (3.13) so uv uses the correct interpreter - Convert [project.optional-dependencies] dev to [dependency-groups] with separate lint/test/dev groups (PEP 735) - Backend lint job uses --only-group lint (installs only ruff, avoids building pyarrow/pandas/langchain entirely) - Backend test job uses --group test (main deps + pytest) - Regenerate uv.lock for new dependency structure --------- Co-authored-by: Adrien Reibel <areibel@sqli.com>
1 parent 6c1552d commit e19f391

9 files changed

Lines changed: 178 additions & 172 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ jobs:
2020
- uses: astral-sh/setup-uv@v6
2121
with:
2222
enable-cache: true
23-
- run: uv sync --frozen --dev
24-
- run: uv run ruff check core/ api/ db/ tests/
25-
- run: uv run ruff format --check core/ api/ db/ tests/
23+
- run: uv run --only-group lint ruff check core/ api/ db/ tests/
24+
- run: uv run --only-group lint ruff format --check core/ api/ db/ tests/
2625

2726
# ── Backend: tests ─────────────────────────────────────────────────────
2827
backend-test:
@@ -34,7 +33,7 @@ jobs:
3433
- uses: astral-sh/setup-uv@v6
3534
with:
3635
enable-cache: true
37-
- run: uv sync --frozen --dev
36+
- run: uv sync --frozen --group test
3837
- run: uv run pytest tests/ -v --tb=short
3938

4039
# ── Frontend: lint + typecheck ─────────────────────────────────────────

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13
Lines changed: 145 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,145 @@
1-
'use client'
2-
3-
import { useCallback, useEffect, useRef, useState } from 'react'
4-
5-
import { Button } from '@/components/ui/button'
6-
import { Input } from '@/components/ui/input'
7-
import type { Question } from '@/lib/api-types'
8-
9-
interface QuestionInputProps {
10-
questions: Question[]
11-
onSubmit?: (answers: Record<string, string>) => void
12-
}
13-
14-
export function QuestionInput({ questions, onSubmit }: QuestionInputProps) {
15-
const [selectedAnswers, setSelectedAnswers] = useState<Record<string, string>>({})
16-
const [showOther, setShowOther] = useState<Record<number, boolean>>({})
17-
const [otherValues, setOtherValues] = useState<Record<number, string>>({})
18-
const submittedRef = useRef(false)
19-
20-
const isResolved = questions.every((q) => q.selected_answer)
21-
const isInteractive = !!onSubmit && !isResolved && !submittedRef.current
22-
23-
// Auto-submit when all questions have a local answer
24-
useEffect(() => {
25-
if (
26-
!submittedRef.current &&
27-
onSubmit &&
28-
questions.length > 0 &&
29-
questions.every((q) => selectedAnswers[q.question])
30-
) {
31-
submittedRef.current = true
32-
onSubmit(selectedAnswers)
33-
}
34-
}, [selectedAnswers, questions, onSubmit])
35-
36-
const handleOptionClick = useCallback(
37-
(question: Question, label: string) => {
38-
if (submittedRef.current) return
39-
setSelectedAnswers((prev) => ({ ...prev, [question.question]: label }))
40-
},
41-
[]
42-
)
43-
44-
const handleOtherSubmit = useCallback(
45-
(questionIdx: number, question: Question) => {
46-
const value = otherValues[questionIdx]
47-
if (!value?.trim() || submittedRef.current) return
48-
setSelectedAnswers((prev) => ({ ...prev, [question.question]: value.trim() }))
49-
},
50-
[otherValues]
51-
)
52-
53-
return (
54-
<div className="space-y-4 py-1">
55-
{questions.map((q, qi) => {
56-
const localAnswer = selectedAnswers[q.question]
57-
const resolvedAnswer = q.selected_answer
58-
const answered = !!localAnswer || !!resolvedAnswer
59-
60-
return (
61-
<div key={qi} className="space-y-2">
62-
{q.header && (
63-
<p className="text-sm text-muted-foreground">{q.header}</p>
64-
)}
65-
<p className="text-sm font-medium">{q.question}</p>
66-
67-
<div className="flex flex-wrap gap-1.5">
68-
{q.options.map((opt, oi) => {
69-
const isSelected = resolvedAnswer === opt.label || localAnswer === opt.label
70-
71-
if (isInteractive && !localAnswer) {
72-
return (
73-
<Button
74-
key={oi}
75-
variant="outline"
76-
size="sm"
77-
onClick={() => handleOptionClick(q, opt.label)}
78-
title={opt.description || undefined}
79-
className="h-auto px-2 py-1 text-xs border-sky/30 bg-sky/5 hover:bg-sky/10 hover:border-sky/50"
80-
>
81-
{opt.label}
82-
</Button>
83-
)
84-
}
85-
86-
return (
87-
<span
88-
key={oi}
89-
className={`inline-flex rounded-md border px-2 py-1 text-xs ${
90-
isSelected
91-
? 'border-primary bg-primary/10 text-primary font-medium'
92-
: 'border-border/50 text-muted-foreground'
93-
}`}
94-
>
95-
{opt.label}
96-
</span>
97-
)
98-
})}
99-
{isInteractive && !localAnswer && (
100-
<Button
101-
variant="ghost"
102-
size="sm"
103-
onClick={() => setShowOther((prev) => ({ ...prev, [qi]: true }))}
104-
className="h-auto px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
105-
>
106-
Other
107-
</Button>
108-
)}
109-
</div>
110-
111-
{/* Free-text answer that doesn't match any option */}
112-
{(resolvedAnswer || localAnswer) && !q.options.some((o) => o.label === (resolvedAnswer || localAnswer)) && (
113-
<p className="text-xs text-primary italic">&rarr; {resolvedAnswer || localAnswer}</p>
114-
)}
115-
116-
{isInteractive && !localAnswer && showOther[qi] && (
117-
<div className="flex gap-2">
118-
<Input
119-
placeholder="Your answer..."
120-
value={otherValues[qi] || ''}
121-
onChange={(e) =>
122-
setOtherValues((prev) => ({ ...prev, [qi]: e.target.value }))
123-
}
124-
onKeyDown={(e) => {
125-
if (e.key === 'Enter') {
126-
e.preventDefault()
127-
handleOtherSubmit(qi, q)
128-
}
129-
}}
130-
/>
131-
<Button
132-
size="sm"
133-
onClick={() => handleOtherSubmit(qi, q)}
134-
disabled={!otherValues[qi]?.trim()}
135-
>
136-
Submit
137-
</Button>
138-
</div>
139-
)}
140-
</div>
141-
)
142-
})}
143-
</div>
144-
)
145-
}
1+
'use client'
2+
3+
import { useCallback, useEffect, useRef, useState } from 'react'
4+
5+
import { Button } from '@/components/ui/button'
6+
import { Input } from '@/components/ui/input'
7+
import type { Question } from '@/lib/api-types'
8+
9+
interface QuestionInputProps {
10+
questions: Question[]
11+
onSubmit?: (answers: Record<string, string>) => void
12+
}
13+
14+
export function QuestionInput({ questions, onSubmit }: QuestionInputProps) {
15+
const [selectedAnswers, setSelectedAnswers] = useState<Record<string, string>>({})
16+
const [showOther, setShowOther] = useState<Record<number, boolean>>({})
17+
const [otherValues, setOtherValues] = useState<Record<number, string>>({})
18+
const submittedRef = useRef(false)
19+
20+
const isResolved = questions.every((q) => q.selected_answer)
21+
const isInteractive = !!onSubmit && !isResolved
22+
23+
// Auto-submit when all questions have a local answer
24+
useEffect(() => {
25+
if (
26+
!submittedRef.current &&
27+
onSubmit &&
28+
questions.length > 0 &&
29+
questions.every((q) => selectedAnswers[q.question])
30+
) {
31+
submittedRef.current = true
32+
onSubmit(selectedAnswers)
33+
}
34+
}, [selectedAnswers, questions, onSubmit])
35+
36+
const handleOptionClick = useCallback(
37+
(question: Question, label: string) => {
38+
if (submittedRef.current) return
39+
setSelectedAnswers((prev) => ({ ...prev, [question.question]: label }))
40+
},
41+
[]
42+
)
43+
44+
const handleOtherSubmit = useCallback(
45+
(questionIdx: number, question: Question) => {
46+
const value = otherValues[questionIdx]
47+
if (!value?.trim() || submittedRef.current) return
48+
setSelectedAnswers((prev) => ({ ...prev, [question.question]: value.trim() }))
49+
},
50+
[otherValues]
51+
)
52+
53+
return (
54+
<div className="space-y-4 py-1">
55+
{questions.map((q, qi) => {
56+
const localAnswer = selectedAnswers[q.question]
57+
const resolvedAnswer = q.selected_answer
58+
const answered = !!localAnswer || !!resolvedAnswer
59+
60+
return (
61+
<div key={qi} className="space-y-2">
62+
{q.header && (
63+
<p className="text-sm text-muted-foreground">{q.header}</p>
64+
)}
65+
<p className="text-sm font-medium">{q.question}</p>
66+
67+
<div className="flex flex-wrap gap-1.5">
68+
{q.options.map((opt, oi) => {
69+
const isSelected = resolvedAnswer === opt.label || localAnswer === opt.label
70+
71+
if (isInteractive && !localAnswer) {
72+
return (
73+
<Button
74+
key={oi}
75+
variant="outline"
76+
size="sm"
77+
onClick={() => handleOptionClick(q, opt.label)}
78+
title={opt.description || undefined}
79+
className="h-auto px-2 py-1 text-xs border-sky/30 bg-sky/5 hover:bg-sky/10 hover:border-sky/50"
80+
>
81+
{opt.label}
82+
</Button>
83+
)
84+
}
85+
86+
return (
87+
<span
88+
key={oi}
89+
className={`inline-flex rounded-md border px-2 py-1 text-xs ${
90+
isSelected
91+
? 'border-primary bg-primary/10 text-primary font-medium'
92+
: 'border-border/50 text-muted-foreground'
93+
}`}
94+
>
95+
{opt.label}
96+
</span>
97+
)
98+
})}
99+
{isInteractive && !localAnswer && (
100+
<Button
101+
variant="ghost"
102+
size="sm"
103+
onClick={() => setShowOther((prev) => ({ ...prev, [qi]: true }))}
104+
className="h-auto px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
105+
>
106+
Other
107+
</Button>
108+
)}
109+
</div>
110+
111+
{/* Free-text answer that doesn't match any option */}
112+
{(resolvedAnswer || localAnswer) && !q.options.some((o) => o.label === (resolvedAnswer || localAnswer)) && (
113+
<p className="text-xs text-primary italic">&rarr; {resolvedAnswer || localAnswer}</p>
114+
)}
115+
116+
{isInteractive && !localAnswer && showOther[qi] && (
117+
<div className="flex gap-2">
118+
<Input
119+
placeholder="Your answer..."
120+
value={otherValues[qi] || ''}
121+
onChange={(e) =>
122+
setOtherValues((prev) => ({ ...prev, [qi]: e.target.value }))
123+
}
124+
onKeyDown={(e) => {
125+
if (e.key === 'Enter') {
126+
e.preventDefault()
127+
handleOtherSubmit(qi, q)
128+
}
129+
}}
130+
/>
131+
<Button
132+
size="sm"
133+
onClick={() => handleOtherSubmit(qi, q)}
134+
disabled={!otherValues[qi]?.trim()}
135+
>
136+
Submit
137+
</Button>
138+
</div>
139+
)}
140+
</div>
141+
)
142+
})}
143+
</div>
144+
)
145+
}

frontend/components/dashboard/blocks/portaled-components.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,6 @@ SelectItem.displayName = 'SelectItem'
323323
*
324324
* The `any` cast is required because the TS type is not exported from @blocknote/shadcn.
325325
*/
326-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
327326
export const portaledShadCNComponents: any = {
328327
Popover: {
329328
Popover,

frontend/components/dashboard/card-editor.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,24 +77,21 @@ function CardEditor({ card, onContentChange, onEditorResize }: CardEditorProps)
7777
useEffect(() => {
7878
cardRegistry.set(card.id, card)
7979
return () => { cardRegistry.delete(card.id) }
80-
}, [card.id, card.type, card.title, card.value, card.fig])
80+
}, [card])
8181

8282
const initialContent = useMemo(
8383
() => (card.content && card.content.length > 0 ? card.content : defaultContent(card)),
84-
// Only compute once per card ID
85-
// eslint-disable-next-line react-hooks/exhaustive-deps
84+
// eslint-disable-next-line react-hooks/exhaustive-deps -- only compute once per card ID
8685
[card.id]
8786
)
8887

8988
const editor = useCreateBlockNote({
9089
schema: dashboardSchema,
91-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9290
initialContent: initialContent as any,
9391
})
9492
editorRef.current = editor as unknown as { document: unknown[] }
9593

9694
// Flush any pending debounced save when the component unmounts (e.g. tab switch).
97-
// eslint-disable-next-line react-hooks/exhaustive-deps
9895
useEffect(() => {
9996
return () => {
10097
if (saveTimerRef.current) {

frontend/components/dashboard/dashboard-grid.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ export default function DashboardGrid({
8383
// Track cards and callback in refs so ResizeObserver/event handlers
8484
// always read fresh values without re-creating effects.
8585
const cardsRef = useRef(cards)
86-
cardsRef.current = cards
86+
useEffect(() => { cardsRef.current = cards }, [cards])
8787
const onLayoutChangeRef = useRef(onLayoutChange)
88-
onLayoutChangeRef.current = onLayoutChange
88+
useEffect(() => { onLayoutChangeRef.current = onLayoutChange }, [onLayoutChange])
8989

9090
// IDs of cards the user has manually shrunk below content height.
9191
const compactedRef = useRef(new Set<string>())
@@ -115,13 +115,11 @@ export default function DashboardGrid({
115115
})
116116

117117
// Persist layout on user drag stop
118-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
119118
const handleDragStop = useCallback((newLayout: any) => {
120119
onLayoutChange(layoutToItems(newLayout))
121120
}, [onLayoutChange])
122121

123122
// Persist layout on user resize stop + track compaction
124-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
125123
const handleResizeStop = useCallback((newLayout: any, _oldItem: any, newItem: any) => {
126124
// If the user made the card shorter than its content, mark it compacted
127125
// so auto-height doesn't fight the user's choice.

frontend/lib/hooks/use-upload-message.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ const MESSAGES = [
1111
]
1212

1313
export function useUploadMessage(active: boolean) {
14-
const [idx, setIdx] = useState(0)
14+
const [idx, setIdx] = useState(() => Math.floor(Math.random() * MESSAGES.length))
1515

1616
useEffect(() => {
1717
if (!active) return
18-
setIdx(Math.floor(Math.random() * MESSAGES.length))
1918
const id = setInterval(() => {
2019
setIdx((prev) => {
2120
let next: number

0 commit comments

Comments
 (0)