| @@ -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] | ||||