| @@ -96,7 +96,7 @@ class WikiPagesController < ApplicationController | |||
| message = params[:message].presence | |||
| Wiki::Commit.content!(page:, body:, created_user: current_user, message:) | |||
| render json: WikiPageRepr.base(page), status: :created | |||
| render json: WikiPageRepr.base(page).merge(body:), status: :created | |||
| else | |||
| render json: { errors: page.errors.full_messages }, | |||
| status: :unprocessable_entity | |||
| @@ -107,7 +107,7 @@ class WikiPagesController < ApplicationController | |||
| return head :unauthorized unless current_user | |||
| return head :forbidden unless current_user.gte_member? | |||
| title = params[:title]&.strip | |||
| title = params[:title].to_s.strip | |||
| body = params[:body].to_s | |||
| return head :unprocessable_entity if title.blank? || body.blank? | |||
| @@ -126,7 +126,7 @@ class WikiPagesController < ApplicationController | |||
| message:, | |||
| base_revision_id:) | |||
| head :ok | |||
| render json: WikiPageRepr.base(page).merge(body:), status: :created | |||
| end | |||
| def search | |||
| @@ -0,0 +1,57 @@ | |||
| import MarkdownIt from 'markdown-it' | |||
| import { useEffect, useState } from 'react' | |||
| import MdEditor from 'react-markdown-editor-lite' | |||
| import Label from '@/components/common/Label' | |||
| import type { FC } from 'react' | |||
| const mdParser = new MarkdownIt | |||
| type Props = { | |||
| title: string | |||
| body: string | |||
| onSubmit: (title: string, body: string) => void | |||
| forEdit?: boolean } | |||
| export default (({ title: initTitle, body: initBody, onSubmit, forEdit }: Props) => { | |||
| const [title, setTitle] = useState<string> (initTitle) | |||
| const [body, setBody] = useState<string> (initBody) | |||
| useEffect (() => { | |||
| setTitle (initTitle) | |||
| setBody (initBody) | |||
| }, [initTitle, initBody]) | |||
| return ( | |||
| <> | |||
| {/* タイトル */} | |||
| {/* TODO: タグ補完 */} | |||
| <div> | |||
| <Label>タイトル</Label> | |||
| <input | |||
| type="text" | |||
| value={title} | |||
| onChange={e => setTitle (e.target.value)} | |||
| className="w-full border p-2 rounded"/> | |||
| </div> | |||
| {/* 本文 */} | |||
| <div> | |||
| <Label>本文</Label> | |||
| <MdEditor | |||
| value={body} | |||
| style={{ height: '500px' }} | |||
| renderHTML={text => mdParser.render (text)} | |||
| onChange={({ text }) => setBody (text)}/> | |||
| </div> | |||
| {/* 送信 */} | |||
| <button | |||
| onClick={() => onSubmit (title, body)} | |||
| className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> | |||
| {forEdit ? '編輯' : '追加'} | |||
| </button> | |||
| </>) | |||
| }) satisfies FC<Props> | |||
| @@ -1,10 +1,9 @@ | |||
| import { useQueryClient } from '@tanstack/react-query' | |||
| import MarkdownIt from 'markdown-it' | |||
| import { useEffect, useState } from 'react' | |||
| import { Helmet } from 'react-helmet-async' | |||
| import MdEditor from 'react-markdown-editor-lite' | |||
| import { useParams, useNavigate } from 'react-router-dom' | |||
| import WikiEditForm from '@/components/WikiEditForm' | |||
| import MainArea from '@/components/layout/MainArea' | |||
| import { toast } from '@/components/ui/use-toast' | |||
| import { SITE_TITLE } from '@/config' | |||
| @@ -18,8 +17,6 @@ import type { FC } from 'react' | |||
| import type { User, WikiPage } from '@/types' | |||
| const mdParser = new MarkdownIt | |||
| type Props = { user: User | null } | |||
| @@ -37,7 +34,7 @@ export default (({ user }: Props) => { | |||
| const [loading, setLoading] = useState (true) | |||
| const [title, setTitle] = useState ('') | |||
| const handleSubmit = async () => { | |||
| const handleSubmit = async (title: string, body: string) => { | |||
| const formData = new FormData () | |||
| formData.append ('title', title) | |||
| formData.append ('body', body) | |||
| @@ -77,32 +74,11 @@ export default (({ user }: Props) => { | |||
| <h1 className="text-2xl font-bold mb-2">Wiki ページを編輯</h1> | |||
| {loading ? 'Loading...' : ( | |||
| <> | |||
| {/* タイトル */} | |||
| {/* TODO: タグ補完 */} | |||
| <div> | |||
| <label className="block font-semibold mb-1">タイトル</label> | |||
| <input type="text" | |||
| value={title} | |||
| onChange={e => setTitle (e.target.value)} | |||
| className="w-full border p-2 rounded"/> | |||
| </div> | |||
| {/* 本文 */} | |||
| <div> | |||
| <label className="block font-semibold mb-1">本文</label> | |||
| <MdEditor value={body} | |||
| style={{ height: '500px' }} | |||
| renderHTML={text => mdParser.render (text)} | |||
| onChange={({ text }) => setBody (text)}/> | |||
| </div> | |||
| {/* 送信 */} | |||
| <button onClick={handleSubmit} | |||
| className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> | |||
| 編輯 | |||
| </button> | |||
| </>)} | |||
| <WikiEditForm | |||
| title={title} | |||
| body={body} | |||
| onSubmit={handleSubmit} | |||
| forEdit/>)} | |||
| </div> | |||
| </MainArea>) | |||
| }) satisfies FC<Props> | |||
| @@ -1,21 +1,19 @@ | |||
| import MarkdownIt from 'markdown-it' | |||
| import { useState } from 'react' | |||
| import { useQueryClient } from '@tanstack/react-query' | |||
| import { Helmet } from 'react-helmet-async' | |||
| import MdEditor from 'react-markdown-editor-lite' | |||
| import { useLocation, useNavigate } from 'react-router-dom' | |||
| import WikiEditForm from '@/components/WikiEditForm' | |||
| import MainArea from '@/components/layout/MainArea' | |||
| import { toast } from '@/components/ui/use-toast' | |||
| import { SITE_TITLE } from '@/config' | |||
| import { apiPost } from '@/lib/api' | |||
| import { wikiKeys } from '@/lib/queryKeys' | |||
| import Forbidden from '@/pages/Forbidden' | |||
| import 'react-markdown-editor-lite/lib/index.css' | |||
| import type { User, WikiPage } from '@/types' | |||
| const mdParser = new MarkdownIt | |||
| type Props = { user: User | null } | |||
| @@ -26,13 +24,12 @@ export default ({ user }: Props) => { | |||
| const location = useLocation () | |||
| const navigate = useNavigate () | |||
| const qc = useQueryClient () | |||
| const query = new URLSearchParams (location.search) | |||
| const titleQuery = query.get ('title') ?? '' | |||
| const [title, setTitle] = useState (titleQuery) | |||
| const [body, setBody] = useState ('') | |||
| const handleSubmit = async () => { | |||
| const handleSubmit = async (title: string, body: string) => { | |||
| const formData = new FormData | |||
| formData.append ('title', title) | |||
| formData.append ('body', body) | |||
| @@ -40,7 +37,10 @@ export default ({ user }: Props) => { | |||
| try | |||
| { | |||
| const data = await apiPost<WikiPage> ('/wiki', formData, | |||
| { headers: { 'Content-Type': 'multipart/form-data' } }) | |||
| { headers: { 'Content-Type': 'multipart/form-data' } }) | |||
| qc.setQueryData (wikiKeys.show (data.title, { }), | |||
| (prev: WikiPage) => ({ ...prev, title: data.title, body: data.body })) | |||
| qc.invalidateQueries ({ queryKey: wikiKeys.root }) | |||
| toast ({ title: '投稿成功!' }) | |||
| navigate (`/wiki/${ data.title }`) | |||
| } | |||
| @@ -58,30 +58,10 @@ export default ({ user }: Props) => { | |||
| <div className="max-w-xl mx-auto p-4 space-y-4"> | |||
| <h1 className="text-2xl font-bold mb-2">新規 Wiki ページ</h1> | |||
| {/* タイトル */} | |||
| {/* TODO: タグ補完 */} | |||
| <div> | |||
| <label className="block font-semibold mb-1">タイトル</label> | |||
| <input type="text" | |||
| value={title} | |||
| onChange={e => setTitle (e.target.value)} | |||
| className="w-full border p-2 rounded"/> | |||
| </div> | |||
| {/* 本文 */} | |||
| <div> | |||
| <label className="block font-semibold mb-1">本文</label> | |||
| <MdEditor value={body} | |||
| style={{ height: '500px' }} | |||
| renderHTML={text => mdParser.render (text)} | |||
| onChange={({ text }) => setBody (text)}/> | |||
| </div> | |||
| {/* 送信 */} | |||
| <button onClick={handleSubmit} | |||
| className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> | |||
| 追加 | |||
| </button> | |||
| <WikiEditForm | |||
| title={titleQuery} | |||
| body="" | |||
| onSubmit={handleSubmit}/> | |||
| </div> | |||
| </MainArea>) | |||
| } | |||