@@ -1,25 +1,10 @@ | |||
class WikiPagesController < ApplicationController | |||
def show | |||
wiki_page = WikiPage.find(params[:id]) | |||
return head :not_found unless wiki_page | |||
render json: wiki_page.as_json.merge(body: wiki_page.body) | |||
render_wiki_page_or_404 WikiPage.find(params[:id]) | |||
end | |||
def show_by_title | |||
title = params[:title] | |||
version = params[:version].presence | |||
wiki_page = WikiPage.find_by(title:) | |||
return head :not_found unless wiki_page | |||
wiki_page.sha = version | |||
body = wiki_page.body | |||
sha = wiki_page.sha | |||
pred = wiki_page.pred | |||
succ = wiki_page.succ | |||
render json: wiki_page.as_json.merge(body:, sha:, pred:, succ:) | |||
render_wiki_page_or_404 WikiPage.find_by(title: params[:title]) | |||
end | |||
def diff | |||
@@ -117,4 +102,16 @@ class WikiPagesController < ApplicationController | |||
def wiki | |||
@wiki ||= Gollum::Wiki.new(WIKI_PATH) | |||
end | |||
def render_wiki_page_or_404 wiki_page | |||
return head :not_found unless wiki_page | |||
wiki_page.sha = params[:version].presence | |||
body = wiki_page.body | |||
sha = wiki_page.sha | |||
pred = wiki_page.pred | |||
succ = wiki_page.succ | |||
render json: wiki_page.as_json.merge(body:, sha:, pred:, succ:) | |||
end | |||
end |
@@ -20,6 +20,7 @@ class WikiPage < ApplicationRecord | |||
idx = vers.find_index { |ver| ver.id == @sha } | |||
@pred = vers[idx + 1]&.id | |||
@succ = idx.positive? ? vers[idx - 1].id : nil | |||
@updated_at = vers[idx].authored_date | |||
@sha | |||
end | |||
@@ -35,6 +36,10 @@ class WikiPage < ApplicationRecord | |||
@succ | |||
end | |||
def updated_at | |||
@updated_at | |||
end | |||
def body | |||
sha = nil unless @page | |||
@page&.raw_data&.force_encoding('UTF-8') | |||
@@ -10,6 +10,7 @@ import PostDetailPage from '@/pages/PostDetailPage' | |||
import WikiPage from '@/pages/WikiPage' | |||
import WikiNewPage from '@/pages/WikiNewPage' | |||
import WikiEditPage from '@/pages/WikiEditPage' | |||
import WikiDiffPage from '@/pages/WikiDiffPage' | |||
import WikiDetailPage from '@/pages/WikiDetailPage' | |||
import WikiHistoryPage from '@/pages/WikiHistoryPage' | |||
import { API_BASE_URL } from '@/config' | |||
@@ -64,9 +65,10 @@ export default () => { | |||
<Route path="/posts/:id" element={<PostDetailPage user={user} />} /> | |||
<Route path="/tags/:tag" element={<TagPage />} /> | |||
<Route path="/wiki" element={<WikiPage />} /> | |||
<Route path="/wiki/:name" element={<WikiDetailPage />} /> | |||
<Route path="/wiki/:title" element={<WikiDetailPage />} /> | |||
<Route path="/wiki/new" element={<WikiNewPage />} /> | |||
<Route path="/wiki/:id/edit" element={<WikiEditPage />} /> | |||
<Route path="/wiki/:id/diff" element={<WikiDiffPage />} /> | |||
<Route path="/wiki/changes" element={<WikiHistoryPage />} /> | |||
</Routes> | |||
</div> | |||
@@ -1,45 +1,68 @@ | |||
import { useEffect, useState } from 'react' | |||
import { Link, useParams, useNavigate } from 'react-router-dom' | |||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom' | |||
import ReactMarkdown from 'react-markdown' | |||
import axios from 'axios' | |||
import { API_BASE_URL } from '@/config' | |||
import MainArea from '@/components/layout/MainArea' | |||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' | |||
import type { WikiPage } from '@/types' | |||
export default () => { | |||
const { name } = useParams () | |||
const { title } = useParams () | |||
const location = useLocation () | |||
const navigate = useNavigate () | |||
const [markdown, setMarkdown] = useState<string | null> (null) | |||
const [wikiPage, setWikiPage] = useState<WikiPage | null | undefined> (undefined) | |||
const query = new URLSearchParams (location.search) | |||
const version = query.get ('version') | |||
useEffect (() => { | |||
if (/^\d+$/.test (name)) | |||
if (/^\d+$/.test (title)) | |||
{ | |||
void (axios.get (`${ API_BASE_URL }/wiki/${ name }`) | |||
void (axios.get (`${ API_BASE_URL }/wiki/${ title }`) | |||
.then (res => navigate (`/wiki/${ res.data.title }`, { replace: true }))) | |||
return | |||
} | |||
void (axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (name) }`) | |||
void (axios.get (`${ API_BASE_URL }/wiki/title/${ encodeURIComponent (title) }`, version && { params: { version } }) | |||
.then (res => { | |||
setMarkdown (res.data.body) | |||
setWikiPage (res.data) | |||
WikiIdBus.set (res.data.id) | |||
}) | |||
.catch (() => setMarkdown (''))) | |||
}, [name]) | |||
.catch (() => setWikiPage (null))) | |||
}, [title, location.search]) | |||
return ( | |||
<MainArea> | |||
{(wikiPage && version) && ( | |||
<div className="text-sm flex gap-3 items-center justify-center border border-gray-700 rounded px-2 py-1 mb-4"> | |||
{wikiPage.pred ? ( | |||
<Link to={`/wiki/${ title }?version=${ wikiPage.pred }`} | |||
className="text-blue-400 hover:underline"> | |||
< 前 | |||
</Link>) : <>< 前</>} | |||
<span>{wikiPage.updated_at}</span> | |||
{wikiPage.succ ? ( | |||
<Link to={`/wiki/${ title }?version=${ wikiPage.succ }`} | |||
className="text-blue-400 hover:underline"> | |||
後 > | |||
</Link>) : <>後 ></>} | |||
</div>)} | |||
<h1>{title}</h1> | |||
<div className="prose mx-auto p-4"> | |||
{markdown == null ? 'Loading...' : ( | |||
{wikiPage === undefined ? 'Loading...' : ( | |||
<> | |||
<ReactMarkdown components={{ a: ( | |||
({ href, children }) => (['/', '.'].some (e => href?.startsWith (e)) | |||
? <Link to={href!}>{children}</Link> | |||
: <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>)) }}> | |||
{markdown || `このページは存在しません。[新規作成してください](/wiki/new?title=${ name })。`} | |||
{wikiPage?.body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ title })。`} | |||
</ReactMarkdown> | |||
</>)} | |||
</div> | |||
@@ -0,0 +1,37 @@ | |||
import { useEffect, useState } from 'react' | |||
import { Link, useLocation, useParams } from 'react-router-dom' | |||
import axios from 'axios' | |||
import MainArea from '@/components/layout/MainArea' | |||
import { API_BASE_URL } from '@/config' | |||
import type { WikiPageDiff } from '@/types' | |||
export default () => { | |||
const { id } = useParams () | |||
const location = useLocation () | |||
const [diff, setDiff] = useState<WikiPageDiff | null> (null) | |||
const query = new URLSearchParams (location.search) | |||
const from = query.get ('from') | |||
const to = query.get ('to') | |||
useEffect (() => { | |||
void (axios.get (`${ API_BASE_URL }/wiki/${ id }/diff`, { params: { from, to } }) | |||
.then (res => setDiff (res.data))) | |||
}, []) | |||
return ( | |||
<MainArea> | |||
<h1>{diff?.title}</h1> | |||
<div className="prose mx-auto p-4"> | |||
{diff ? ( | |||
diff.diff.map (d => ( | |||
<span className={d.type === 'added' ? 'bg-green-800' : d.type === 'removed' ? 'bg-red-800' : ''}> | |||
{d.content == '\n' ? <br /> : d.content} | |||
</span>))) : 'Loading...'} | |||
</div> | |||
</MainArea>) | |||
} |
@@ -33,10 +33,12 @@ export default () => { | |||
<tbody> | |||
{changes.map (change => ( | |||
<tr key={change.sha}> | |||
<td>{change.change_type === 'update' && ( | |||
<Link> | |||
差分 | |||
</Link>)}</td> | |||
<td> | |||
{change.change_type === 'update' && ( | |||
<Link to={`/wiki/${ change.wiki_page.id }/diff?from=${ change.pred }&to=${ change.sha }`}> | |||
差分 | |||
</Link>)} | |||
</td> | |||
<td className="p-2"> | |||
<Link to={`/wiki/${ encodeURIComponent (change.wiki_page.title) }?version=${ change.sha }`} | |||
className="text-blue-400 hover:underline"> | |||
@@ -25,13 +25,29 @@ export type User = { | |||
export type WikiPage = { | |||
id: number | |||
title: string | |||
sha: string | |||
pred?: string | |||
succ?: string | |||
updated_at?: string } | |||
export type WikiPageChange = { | |||
sha: string | |||
pred?: string | |||
succ?: string | |||
wiki_page: WikiPage | |||
user: User | |||
change_type: string | |||
timestamp: string } | |||
export type WikiPageDiff = { | |||
wiki_page_id: number | |||
title: string | |||
older_sha: string | |||
newer_sha: string | |||
diff: WikiPageDiffDiff[] } | |||
export type WikiPageDiffDiff = { | |||
type: 'context' | 'added' | 'removed' | |||
content: string } | |||
export type UserRole = typeof USER_ROLES[number] |