Skip to content

Commit a659425

Browse files
committed
fix: frondend error.
1 parent 84502b6 commit a659425

9 files changed

Lines changed: 69 additions & 41 deletions

File tree

frontend/eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
55
import { defineConfig, globalIgnores } from 'eslint/config'
66

77
export default defineConfig([
8-
globalIgnores(['dist']),
8+
globalIgnores(['dist', 'coverage']),
99
{
1010
files: ['**/*.{js,jsx}'],
1111
extends: [

frontend/src/api/client.test.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { describe, it, expect, vi, beforeEach } from 'vitest'
1+
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest'
22
import api from './client'
3-
import axios from 'axios'
43

54
describe('API Client', () => {
65
const originalLocation = window.location
@@ -33,14 +32,11 @@ describe('API Client', () => {
3332
}
3433
}
3534

36-
// Try to trigger the error interceptor
3735
try {
38-
// Accessing the private interceptors array might be brittle,
39-
// but it's a way to test it without making real calls.
4036
const interceptor = api.interceptors.response.handlers[0].rejected
4137
await interceptor(error401)
42-
} catch (e) {
43-
// Expected to reject
38+
} catch {
39+
// expected: interceptor rejects after redirect
4440
}
4541

4642
expect(window.location.href).toBe('/login')
@@ -59,7 +55,9 @@ describe('API Client', () => {
5955
try {
6056
const interceptor = api.interceptors.response.handlers[0].rejected
6157
await interceptor(error401)
62-
} catch (e) {}
58+
} catch {
59+
// expected: interceptor rejects without redirect
60+
}
6361

6462
expect(window.location.href).toBe('/login')
6563
})
@@ -79,7 +77,9 @@ describe('API Client', () => {
7977
try {
8078
const interceptor = api.interceptors.response.handlers[0].rejected
8179
await interceptor(error401)
82-
} catch (e) {}
80+
} catch {
81+
// expected: interceptor rejects without redirect
82+
}
8383

8484
// href must stay at the public URL — no hard redirect to /login
8585
expect(window.location.href).toBe(pathname)

frontend/src/components/Comments.jsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,35 @@ export default function Comments({ slug }) {
6161
const [comments, setComments] = useState([])
6262
const [total, setTotal] = useState(0)
6363
const [loading, setLoading] = useState(true)
64+
const [loadedSlug, setLoadedSlug] = useState(slug)
6465
const [newComment, setNewComment] = useState('')
6566
const [submitting, setSubmitting] = useState(false)
6667

68+
// Reset loading when the slug changes (adjusting state during render).
69+
if (loadedSlug !== slug) {
70+
setLoadedSlug(slug)
71+
setLoading(true)
72+
}
73+
6774
const loadComments = async () => {
6875
try {
6976
const res = await api.get(`/pages/${slug}/comments`)
7077
setComments(res.data.comments || [])
7178
setTotal(res.data.total || 0)
7279
} catch { /* ignore */ }
73-
setLoading(false)
7480
}
7581

7682
useEffect(() => {
77-
setLoading(true)
78-
loadComments()
83+
let cancelled = false
84+
api.get(`/pages/${slug}/comments`)
85+
.then((res) => {
86+
if (cancelled) return
87+
setComments(res.data.comments || [])
88+
setTotal(res.data.total || 0)
89+
setLoading(false)
90+
})
91+
.catch(() => { if (!cancelled) setLoading(false) })
92+
return () => { cancelled = true }
7993
}, [slug])
8094

8195
const handleSubmit = async (e) => {

frontend/src/components/Editor/Editor.jsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'
22
import { Editor as MilkdownEditor, rootCtx, defaultValueCtx, commandsCtx } from '@milkdown/kit/core'
3-
import { commonmark, headingSchema, blockquoteSchema, hrSchema, bulletListSchema, orderedListSchema, codeBlockSchema, paragraphSchema, imageSchema, insertImageCommand } from '@milkdown/kit/preset/commonmark'
3+
import { commonmark, headingSchema, blockquoteSchema, hrSchema, bulletListSchema, orderedListSchema, codeBlockSchema, insertImageCommand } from '@milkdown/kit/preset/commonmark'
44
import { clearTextInCurrentBlockCommand, setBlockTypeCommand, wrapInBlockTypeCommand, addBlockTypeCommand } from '@milkdown/kit/preset/commonmark'
55
import { gfm } from '@milkdown/kit/preset/gfm'
66
import { listener, listenerCtx } from '@milkdown/kit/plugin/listener'
77
import { clipboard } from '@milkdown/kit/plugin/clipboard'
88
import { history } from '@milkdown/kit/plugin/history'
9-
import { getMarkdown } from '@milkdown/kit/utils'
109
import { slashFactory, SlashProvider } from '@milkdown/kit/plugin/slash'
1110
import { TextSelection, Plugin, PluginKey } from '@milkdown/kit/prose/state'
1211
import { $prose } from '@milkdown/kit/utils'
@@ -354,10 +353,13 @@ const Editor = forwardRef(function Editor({ defaultValue = '', onChange, onDrawi
354353
const editorRef = useRef(null)
355354
const containerRef = useRef(null)
356355
const onChangeRef = useRef(onChange)
357-
onChangeRef.current = onChange
358356
const drawioHandlerRef = useRef(onDrawioOpen)
359357
const editorViewRef = useRef(null)
360358

359+
useEffect(() => {
360+
onChangeRef.current = onChange
361+
})
362+
361363
useEffect(() => {
362364
drawioHandlerRef.current = onDrawioOpen || null
363365
}, [onDrawioOpen])

frontend/src/components/Layout/Sidebar.jsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState, useRef } from 'react'
1+
import React, { useState, useRef } from 'react'
22
import { Link, useLocation } from 'react-router-dom'
33
import usePages from '../../store/usePages'
44
import useBookmarks from '../../store/useBookmarks'
@@ -9,16 +9,20 @@ function TreeNode({ node, depth = 0, parentId = null, index = 0 }) {
99
const { movePage } = usePages()
1010
const isActive = location.pathname === `/page/${node.slug}`
1111
const hasChildren = node.children?.length > 0
12-
const [expanded, setExpanded] = useState(isActive || depth < 1)
12+
const [expanded, setExpanded] = useState(
13+
() => isActive || depth < 1 || (hasChildren && isChildActive(node, location.pathname))
14+
)
15+
const [trackedPath, setTrackedPath] = useState(location.pathname)
1316
const [dropPosition, setDropPosition] = useState(null) // 'before' | 'inside' | 'after'
1417
const rowRef = useRef(null)
1518

16-
// Auto-expand if a child is active
17-
useEffect(() => {
19+
// Auto-expand when navigation lands on a descendant (adjusting state during render).
20+
if (trackedPath !== location.pathname) {
21+
setTrackedPath(location.pathname)
1822
if (hasChildren && isChildActive(node, location.pathname)) {
1923
setExpanded(true)
2024
}
21-
}, [location.pathname])
25+
}
2226

2327
const handleDragStart = (e) => {
2428
e.dataTransfer.setData('application/json', JSON.stringify({

frontend/src/components/Search/SearchModal.jsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
1-
import { useState, useEffect, useRef, useCallback } from 'react'
1+
import { useState, useEffect, useRef, useMemo } from 'react'
22
import { useNavigate } from 'react-router-dom'
33
import useSearch from '../../store/useSearch'
44

55
export default function SearchModal({ isOpen, onClose }) {
66
const [query, setQuery] = useState('')
77
const [selectedIdx, setSelectedIdx] = useState(0)
8+
const [wasOpen, setWasOpen] = useState(isOpen)
89
const inputRef = useRef(null)
910
const navigate = useNavigate()
1011
const { results, loading, search, clearSearch } = useSearch()
1112

12-
useEffect(() => {
13+
// Reset form each time the modal transitions closed → open (adjusting state during render).
14+
if (wasOpen !== isOpen) {
15+
setWasOpen(isOpen)
1316
if (isOpen) {
1417
setQuery('')
1518
clearSearch()
1619
setSelectedIdx(0)
17-
setTimeout(() => inputRef.current?.focus(), 50)
1820
}
21+
}
22+
23+
useEffect(() => {
24+
if (!isOpen) return
25+
const t = setTimeout(() => inputRef.current?.focus(), 50)
26+
return () => clearTimeout(t)
1927
}, [isOpen])
2028

21-
const doSearch = useCallback(
22-
debounce((q) => {
29+
const doSearch = useMemo(
30+
() => debounce((q) => {
2331
if (q.trim()) search(q.trim())
2432
else clearSearch()
2533
}, 300),
26-
[]
34+
[search, clearSearch]
2735
)
2836

2937
const handleInput = (e) => {

frontend/src/pages/Admin.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,13 @@ function UsersSection() {
9090
} catch { /* ignore */ }
9191
}
9292

93-
useEffect(() => { loadUsers() }, [])
93+
useEffect(() => {
94+
let cancelled = false
95+
api.get('/users')
96+
.then((res) => { if (!cancelled) setUsers(res.data.users || []) })
97+
.catch(() => { /* ignore */ })
98+
return () => { cancelled = true }
99+
}, [])
94100

95101
const handleCreate = async (e) => {
96102
e.preventDefault()

frontend/src/pages/NewPage.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export default function NewPage() {
9292
setError(err?.response?.data?.detail || err.message || 'Create failed')
9393
setSaving(false)
9494
}
95-
}, [title, content, saving, selectedTemplate])
95+
}, [title, content, saving, selectedTemplate, createPage, fetchTree, navigate])
9696

9797
// Ctrl+S
9898
useEffect(() => {

frontend/src/pages/PageEdit.jsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ export default function PageEdit() {
1818
const [saving, setSaving] = useState(false)
1919
const [error, setError] = useState('')
2020
const [conflict, setConflict] = useState(null) // { currentVersion } on 409
21-
const [dirty, setDirty] = useState(false)
22-
const originalRef = useRef({ title: '', content: '' })
21+
const [original, setOriginal] = useState({ title: '', content: '' })
2322
const baseVersionRef = useRef(null)
2423
const editorRef = useRef(null)
2524

25+
const dirty = !!page && (title !== original.title || content !== original.content)
26+
2627
// Preview state
2728
const [showPreview, setShowPreview] = useState(false)
2829

@@ -38,19 +39,13 @@ export default function PageEdit() {
3839
setPage(p)
3940
setTitle(p.title)
4041
setContent(p.content_md)
41-
originalRef.current = { title: p.title, content: p.content_md }
42+
setOriginal({ title: p.title, content: p.content_md })
4243
baseVersionRef.current = p.version
4344
}).catch(() => {
4445
navigate('/')
4546
})
4647
}, [slug])
4748

48-
useEffect(() => {
49-
if (!page) return
50-
const { title: origTitle, content: origContent } = originalRef.current
51-
setDirty(title !== origTitle || content !== origContent)
52-
}, [title, content, page])
53-
5449
// Extract diagram IDs from content and fetch their data
5550
const diagramIds = useMemo(() => {
5651
const ids = []
@@ -142,7 +137,7 @@ export default function PageEdit() {
142137
})
143138
baseVersionRef.current = updated.version
144139
await fetchTree()
145-
setDirty(false)
140+
setOriginal({ title, content })
146141
setSaving(false)
147142
navigate(`/page/${slug}`)
148143
} catch (err) {
@@ -171,10 +166,9 @@ export default function PageEdit() {
171166
if (confirm('Discard your changes and load the latest version?')) {
172167
setTitle(latest.title)
173168
setContent(latest.content_md)
174-
originalRef.current = { title: latest.title, content: latest.content_md }
169+
setOriginal({ title: latest.title, content: latest.content_md })
175170
baseVersionRef.current = latest.version
176171
setConflict(null)
177-
setDirty(false)
178172
}
179173
} catch (e) {
180174
console.error('Failed to reload:', e)

0 commit comments

Comments
 (0)