@@ -1,25 +1,10 @@ | |||||
class WikiPagesController < ApplicationController | class WikiPagesController < ApplicationController | ||||
def show | 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 | end | ||||
def show_by_title | 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 | end | ||||
def diff | def diff | ||||
@@ -117,4 +102,16 @@ class WikiPagesController < ApplicationController | |||||
def wiki | def wiki | ||||
@wiki ||= Gollum::Wiki.new(WIKI_PATH) | @wiki ||= Gollum::Wiki.new(WIKI_PATH) | ||||
end | 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 | end |
@@ -20,6 +20,7 @@ class WikiPage < ApplicationRecord | |||||
idx = vers.find_index { |ver| ver.id == @sha } | idx = vers.find_index { |ver| ver.id == @sha } | ||||
@pred = vers[idx + 1]&.id | @pred = vers[idx + 1]&.id | ||||
@succ = idx.positive? ? vers[idx - 1].id : nil | @succ = idx.positive? ? vers[idx - 1].id : nil | ||||
@updated_at = vers[idx].authored_date | |||||
@sha | @sha | ||||
end | end | ||||
@@ -35,6 +36,10 @@ class WikiPage < ApplicationRecord | |||||
@succ | @succ | ||||
end | end | ||||
def updated_at | |||||
@updated_at | |||||
end | |||||
def body | def body | ||||
sha = nil unless @page | sha = nil unless @page | ||||
@page&.raw_data&.force_encoding('UTF-8') | @page&.raw_data&.force_encoding('UTF-8') | ||||
@@ -10,6 +10,7 @@ import PostDetailPage from '@/pages/PostDetailPage' | |||||
import WikiPage from '@/pages/WikiPage' | import WikiPage from '@/pages/WikiPage' | ||||
import WikiNewPage from '@/pages/WikiNewPage' | import WikiNewPage from '@/pages/WikiNewPage' | ||||
import WikiEditPage from '@/pages/WikiEditPage' | import WikiEditPage from '@/pages/WikiEditPage' | ||||
import WikiDiffPage from '@/pages/WikiDiffPage' | |||||
import WikiDetailPage from '@/pages/WikiDetailPage' | import WikiDetailPage from '@/pages/WikiDetailPage' | ||||
import WikiHistoryPage from '@/pages/WikiHistoryPage' | import WikiHistoryPage from '@/pages/WikiHistoryPage' | ||||
import { API_BASE_URL } from '@/config' | import { API_BASE_URL } from '@/config' | ||||
@@ -64,9 +65,10 @@ export default () => { | |||||
<Route path="/posts/:id" element={<PostDetailPage user={user} />} /> | <Route path="/posts/:id" element={<PostDetailPage user={user} />} /> | ||||
<Route path="/tags/:tag" element={<TagPage />} /> | <Route path="/tags/:tag" element={<TagPage />} /> | ||||
<Route path="/wiki" element={<WikiPage />} /> | <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/new" element={<WikiNewPage />} /> | ||||
<Route path="/wiki/:id/edit" element={<WikiEditPage />} /> | <Route path="/wiki/:id/edit" element={<WikiEditPage />} /> | ||||
<Route path="/wiki/:id/diff" element={<WikiDiffPage />} /> | |||||
<Route path="/wiki/changes" element={<WikiHistoryPage />} /> | <Route path="/wiki/changes" element={<WikiHistoryPage />} /> | ||||
</Routes> | </Routes> | ||||
</div> | </div> | ||||
@@ -1,45 +1,68 @@ | |||||
import { useEffect, useState } from 'react' | 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 ReactMarkdown from 'react-markdown' | ||||
import axios from 'axios' | import axios from 'axios' | ||||
import { API_BASE_URL } from '@/config' | import { API_BASE_URL } from '@/config' | ||||
import MainArea from '@/components/layout/MainArea' | import MainArea from '@/components/layout/MainArea' | ||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' | import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' | ||||
import type { WikiPage } from '@/types' | |||||
export default () => { | export default () => { | ||||
const { name } = useParams () | |||||
const { title } = useParams () | |||||
const location = useLocation () | |||||
const navigate = useNavigate () | 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 (() => { | 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 }))) | .then (res => navigate (`/wiki/${ res.data.title }`, { replace: true }))) | ||||
return | 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 => { | .then (res => { | ||||
setMarkdown (res.data.body) | |||||
setWikiPage (res.data) | |||||
WikiIdBus.set (res.data.id) | WikiIdBus.set (res.data.id) | ||||
}) | }) | ||||
.catch (() => setMarkdown (''))) | |||||
}, [name]) | |||||
.catch (() => setWikiPage (null))) | |||||
}, [title, location.search]) | |||||
return ( | return ( | ||||
<MainArea> | <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"> | <div className="prose mx-auto p-4"> | ||||
{markdown == null ? 'Loading...' : ( | |||||
{wikiPage === undefined ? 'Loading...' : ( | |||||
<> | <> | ||||
<ReactMarkdown components={{ a: ( | <ReactMarkdown components={{ a: ( | ||||
({ href, children }) => (['/', '.'].some (e => href?.startsWith (e)) | ({ href, children }) => (['/', '.'].some (e => href?.startsWith (e)) | ||||
? <Link to={href!}>{children}</Link> | ? <Link to={href!}>{children}</Link> | ||||
: <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>)) }}> | : <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>)) }}> | ||||
{markdown || `このページは存在しません。[新規作成してください](/wiki/new?title=${ name })。`} | |||||
{wikiPage?.body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ title })。`} | |||||
</ReactMarkdown> | </ReactMarkdown> | ||||
</>)} | </>)} | ||||
</div> | </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> | <tbody> | ||||
{changes.map (change => ( | {changes.map (change => ( | ||||
<tr key={change.sha}> | <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"> | <td className="p-2"> | ||||
<Link to={`/wiki/${ encodeURIComponent (change.wiki_page.title) }?version=${ change.sha }`} | <Link to={`/wiki/${ encodeURIComponent (change.wiki_page.title) }?version=${ change.sha }`} | ||||
className="text-blue-400 hover:underline"> | className="text-blue-400 hover:underline"> | ||||
@@ -25,13 +25,29 @@ export type User = { | |||||
export type WikiPage = { | export type WikiPage = { | ||||
id: number | id: number | ||||
title: string | title: string | ||||
sha: string | |||||
pred?: string | |||||
succ?: string | |||||
updated_at?: string } | updated_at?: string } | ||||
export type WikiPageChange = { | export type WikiPageChange = { | ||||
sha: string | sha: string | ||||
pred?: string | |||||
succ?: string | |||||
wiki_page: WikiPage | wiki_page: WikiPage | ||||
user: User | user: User | ||||
change_type: string | change_type: string | ||||
timestamp: 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] | export type UserRole = typeof USER_ROLES[number] |