Files
website/client/src/routes/admin/views/WikiHistory.jsx
wtclaude 12d50fd615
All checks were successful
PR Checks / bot-install (pull_request) Successful in 13s
PR Checks / client-build (pull_request) Successful in 22s
PR Checks / server-tests (pull_request) Successful in 11m13s
chore(quality): resolve SonarQube code smells across website
Clears the 124 CODE_SMELL findings from the SonarQube scan (server, client,
and bot). All changes are behaviour-preserving refactors — no route, protocol,
schema, or config changes — verified against the full server (381) and client
(43) test suites plus a clean client build.

By rule:
- S3776 (20, cognitive complexity): extract helpers/handlers so each function
  drops under the threshold — shard model upsert builders, page/wiki update,
  block validation, notification stream mapping (dispatch table), SSO mobile
  login, shard ingest deps, uo-link socket backfill/connect, the bot slash-
  command dispatchers + discord manager, and the Shard/UserDetail/HeroEditor/
  CharacterStats React components.
- S4624 (34, nested template literals): pull inner templates into locals /
  a withQs() helper; rewrite shardEvents.describe() as a formatter table.
- S3358 (35, nested ternaries): lift to if/else vars, lookup maps, small
  components, or guarded JSX expressions.
- S6479 (12, array-index React keys): key by stable content instead of index
  (two in-editor lists left as-is; index matches their by-index edit model).
- S6353 (6): [0-9]/[^0-9] -> \d/\D.  S125 (5): reword state-shape comments that
  parsed as code.  S3800/S3782 (botScore): JSDoc-type PATH_WEIGHTS tuples.
- S6481 (2): memoize Auth/Site context values (and SiteContext brand).
- S4144: dedupe HeroEditor upload handler into useImageUpload().
- S1126 (2), S6035, S5869 (redundant A-Z under /i), S5843 (town-name regex ->
  prefix list): assorted one-liners.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 04:35:39 -05:00

145 lines
4.9 KiB
JavaScript

import { useEffect, useMemo, useState } from 'react'
import { diffWords } from 'diff'
import Modal from '../../../components/Modal.jsx'
import { dateTime } from '../../../lib/format.js'
import { api } from '../../../api/client.js'
// Plain-text view of a body, for a readable word-level diff.
function toText(html) {
if (!html) return ''
if (typeof window === 'undefined' || !window.DOMParser) return html
const doc = new DOMParser().parseFromString(html, 'text/html')
return doc.body.textContent || ''
}
export default function WikiHistory({ slug, onClose, onRestored }) {
const [revisions, setRevisions] = useState([])
const [current, setCurrent] = useState(null)
const [selected, setSelected] = useState(null) // full revision snapshot
const [loading, setLoading] = useState(true)
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
let active = true
Promise.all([api.admin.listWikiRevisions(slug), api.admin.getWiki(slug)])
.then(([revs, page]) => {
if (!active) return
setRevisions(revs)
setCurrent(page)
})
.catch(() => active && setError('Could not load history.'))
.finally(() => active && setLoading(false))
return () => {
active = false
}
}, [slug])
async function selectRevision(id) {
setError('')
try {
const rev = await api.admin.getWikiRevision(slug, id)
setSelected(rev)
} catch (err) {
setError(err.message || 'Could not load revision.')
}
}
async function restore() {
if (!selected) return
if (!confirm(`Restore the page to revision #${selected.id}? This creates a new revision.`)) return
setBusy(true)
try {
await api.admin.restoreWikiRevision(slug, selected.id)
onRestored()
} catch (err) {
setError(err.message || 'Could not restore.')
setBusy(false)
}
}
// Diff the selected revision (old) against the current saved page (new).
const parts = useMemo(
() => (selected ? diffWords(toText(selected.body), toText(current?.body || '')) : []),
[selected, current],
)
return (
<Modal
title={`History — ${slug}`}
onClose={onClose}
width={760}
footer={
<>
<button onClick={onClose} disabled={busy} className="pill">
Close
</button>
<button onClick={restore} disabled={busy || !selected} className="btn btn-primary btn-sq">
{busy ? 'Restoring…' : 'Restore this revision'}
</button>
</>
}
>
{loading && <span className="spin" />}
{!loading && error && (
<p className="sans" style={{ margin: 0, color: '#d98b84', fontSize: '0.85rem' }}>{error}</p>
)}
{!loading && !error && (
<div className="wiki-history">
<ul className="wiki-history-list">
{revisions.map((r, i) => (
<li key={r.id}>
<button
type="button"
className={`wiki-rev${selected?.id === r.id ? ' is-active' : ''}`}
onClick={() => selectRevision(r.id)}
>
<span className="wiki-rev-note">
{r.change_note || 'Edit'}
{i === 0 && <span className="wiki-rev-latest"> · latest</span>}
</span>
<span className="wiki-rev-meta">
{dateTime(r.created_at)}
{r.editor ? ` · ${r.editor}` : ''}
</span>
</button>
</li>
))}
{revisions.length === 0 && <li className="dim sans" style={{ fontSize: '0.85rem' }}>No revisions yet.</li>}
</ul>
<div className="wiki-history-diff">
{!selected ? (
<p className="muted sans" style={{ fontSize: '0.9rem' }}>
Select a revision to see what changed between it and the current page.
</p>
) : (
<>
<p className="sans dim" style={{ margin: '0 0 10px', fontSize: '0.76rem' }}>
Diff: revision #{selected.id} current
</p>
<div className="wiki-diff">
{parts.length === 0 || (parts.length === 1 && !parts[0].added && !parts[0].removed) ? (
<span className="muted">No textual differences.</span>
) : (
parts.map((p, i) => {
let cls = ''
if (p.added) cls = 'diff-add'
else if (p.removed) cls = 'diff-del'
return (
<span key={`${i}:${p.value}`} className={cls}>
{p.value}
</span>
)
})
)}
</div>
</>
)}
</div>
</div>
)}
</Modal>
)
}