このコミットが含まれているのは:
@@ -7,7 +7,6 @@ import PostList from '@/components/PostList'
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import TagSidebar from '@/components/TagSidebar'
|
||||
import WikiBody from '@/components/WikiBody'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import Pagination from '@/components/common/Pagination'
|
||||
import TabGroup, { Tab } from '@/components/common/TabGroup'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
@@ -16,43 +15,21 @@ import { fetchPosts } from '@/lib/posts'
|
||||
import { postsKeys } from '@/lib/queryKeys'
|
||||
import {
|
||||
DEFAULT_POST_LIST_LIMIT,
|
||||
DEFAULT_POST_LIST_ORDER,
|
||||
getClientListLimit,
|
||||
getClientListOrder,
|
||||
LIST_LIMIT_OPTIONS,
|
||||
POST_LIST_ORDER_OPTIONS,
|
||||
setClientListSettings,
|
||||
} from '@/lib/settings'
|
||||
import { inputClass } from '@/lib/utils'
|
||||
import { fetchWikiPageByTitle } from '@/lib/wiki'
|
||||
|
||||
import type { FC } from 'react'
|
||||
|
||||
import type { FetchPostsOrder, WikiPage } from '@/types'
|
||||
|
||||
const postOrderLabel: Record<FetchPostsOrder, string> = {
|
||||
'title:asc': 'タイトル昇順',
|
||||
'title:desc': 'タイトル降順',
|
||||
'url:asc': 'URL 昇順',
|
||||
'url:desc': 'URL 降順',
|
||||
'original_created_at:asc': 'オリジナル投稿日時 昇順',
|
||||
'original_created_at:desc': 'オリジナル投稿日時 降順',
|
||||
'created_at:asc': '投稿日 昇順',
|
||||
'created_at:desc': '投稿日 降順',
|
||||
'updated_at:asc': '更新日 昇順',
|
||||
'updated_at:desc': '更新日 降順',
|
||||
}
|
||||
import type { WikiPage } from '@/types'
|
||||
|
||||
const parseLimit = (value: string | null): 20 | 50 | 100 | null => {
|
||||
const n = Number (value)
|
||||
return n === 20 || n === 50 || n === 100 ? n : null
|
||||
}
|
||||
|
||||
const parseOrder = (value: string | null): FetchPostsOrder | null =>
|
||||
POST_LIST_ORDER_OPTIONS.some (option => option === value)
|
||||
? value as FetchPostsOrder
|
||||
: null
|
||||
|
||||
|
||||
const PostListPage: FC = () => {
|
||||
const containerRef = useRef<HTMLDivElement | null> (null)
|
||||
@@ -73,11 +50,7 @@ const PostListPage: FC = () => {
|
||||
?? getClientListLimit ('postList')
|
||||
?? DEFAULT_POST_LIST_LIMIT
|
||||
)
|
||||
const order = (
|
||||
parseOrder (query.get ('order'))
|
||||
?? getClientListOrder<FetchPostsOrder> ('postList')
|
||||
?? DEFAULT_POST_LIST_ORDER
|
||||
)
|
||||
const order = 'original_created_at:desc' as const
|
||||
|
||||
const keys = {
|
||||
tags: tagsKey, match, page, limit,
|
||||
@@ -92,14 +65,12 @@ const PostListPage: FC = () => {
|
||||
const totalPages = data ? Math.ceil (data.count / limit) : 0
|
||||
|
||||
useEffect (() => {
|
||||
setClientListSettings ('postList', { limit, order })
|
||||
}, [limit, order])
|
||||
setClientListSettings ('postList', { limit })
|
||||
}, [limit])
|
||||
|
||||
const updateListQuery = (next: { limit?: 20 | 50 | 100
|
||||
order?: FetchPostsOrder }) => {
|
||||
const updateListQuery = (next: { limit?: 20 | 50 | 100 }) => {
|
||||
const params = new URLSearchParams (location.search)
|
||||
params.set ('limit', String (next.limit ?? limit))
|
||||
params.set ('order', next.order ?? order)
|
||||
params.set ('page', '1')
|
||||
navigate (`${ location.pathname }?${ params.toString () }`)
|
||||
}
|
||||
@@ -146,39 +117,24 @@ const PostListPage: FC = () => {
|
||||
}}/>
|
||||
|
||||
<MainArea>
|
||||
<div className="mb-4 flex flex-wrap gap-4 rounded-xl border border-border bg-background/80 p-4">
|
||||
<FormField label="表示件数">
|
||||
{() => (
|
||||
<select
|
||||
className={inputClass (false)}
|
||||
value={limit}
|
||||
onChange={ev => updateListQuery ({
|
||||
limit: Number (ev.target.value) as 20 | 50 | 100,
|
||||
})}>
|
||||
{LIST_LIMIT_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>
|
||||
{value} 件
|
||||
</option>))}
|
||||
</select>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="並び順">
|
||||
{() => (
|
||||
<select
|
||||
className={inputClass (false)}
|
||||
value={order}
|
||||
onChange={ev => updateListQuery ({
|
||||
order: ev.target.value as FetchPostsOrder,
|
||||
})}>
|
||||
{POST_LIST_ORDER_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>
|
||||
{postOrderLabel[value]}
|
||||
</option>))}
|
||||
</select>)}
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<TabGroup>
|
||||
<TabGroup
|
||||
rightAddon={
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>件数</span>
|
||||
<select
|
||||
className="rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-foreground"
|
||||
value={limit}
|
||||
onChange={ev => updateListQuery ({
|
||||
limit: Number (ev.target.value) as 20 | 50 | 100,
|
||||
})}>
|
||||
{LIST_LIMIT_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>))}
|
||||
</select>
|
||||
</label>
|
||||
}>
|
||||
<Tab name="広場">
|
||||
{posts.length > 0
|
||||
? (
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
getClientListLimit,
|
||||
getClientListOrder,
|
||||
LIST_LIMIT_OPTIONS,
|
||||
POST_LIST_ORDER_OPTIONS,
|
||||
setClientListSettings,
|
||||
} from '@/lib/settings'
|
||||
import { dateString, inputClass, originalCreatedAtString } from '@/lib/utils'
|
||||
@@ -40,28 +39,30 @@ const setIf = (qs: URLSearchParams, k: string, v: string | null) => {
|
||||
qs.set (k, t)
|
||||
}
|
||||
|
||||
const postOrderLabel: Record<FetchPostsOrder, string> = {
|
||||
'title:asc': 'タイトル昇順',
|
||||
'title:desc': 'タイトル降順',
|
||||
'url:asc': 'URL 昇順',
|
||||
'url:desc': 'URL 降順',
|
||||
'original_created_at:asc': 'オリジナル投稿日時 昇順',
|
||||
'original_created_at:desc': 'オリジナル投稿日時 降順',
|
||||
'created_at:asc': '投稿日 昇順',
|
||||
'created_at:desc': '投稿日 降順',
|
||||
'updated_at:asc': '更新日 昇順',
|
||||
'updated_at:desc': '更新日 降順',
|
||||
}
|
||||
|
||||
const parseLimit = (value: string | null): 20 | 50 | 100 | null => {
|
||||
const n = Number (value)
|
||||
return n === 20 || n === 50 || n === 100 ? n : null
|
||||
}
|
||||
|
||||
const parseOrder = (value: string | null): FetchPostsOrder | null =>
|
||||
POST_LIST_ORDER_OPTIONS.some (option => option === value)
|
||||
? value as FetchPostsOrder
|
||||
: null
|
||||
const parseOrder = (value: string | null): FetchPostsOrder | null => {
|
||||
switch (value)
|
||||
{
|
||||
case 'title:asc':
|
||||
case 'title:desc':
|
||||
case 'url:asc':
|
||||
case 'url:desc':
|
||||
case 'original_created_at:asc':
|
||||
case 'original_created_at:desc':
|
||||
case 'created_at:asc':
|
||||
case 'created_at:desc':
|
||||
case 'updated_at:asc':
|
||||
case 'updated_at:desc':
|
||||
return value
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const PostSearchPage: FC = () => {
|
||||
@@ -188,41 +189,26 @@ const PostSearchPage: FC = () => {
|
||||
</Helmet>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<PageTitle>広場検索</PageTitle>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<PageTitle>広場検索</PageTitle>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>件数</span>
|
||||
<select
|
||||
className="rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-foreground"
|
||||
value={limit}
|
||||
onChange={ev => updateListQuery ({
|
||||
limit: Number (ev.target.value) as 20 | 50 | 100,
|
||||
})}>
|
||||
{LIST_LIMIT_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch} className="space-y-2">
|
||||
<div className="grid gap-3 rounded-xl border border-border bg-background/80 p-3 sm:grid-cols-2">
|
||||
<FormField label="表示件数">
|
||||
{() => (
|
||||
<select
|
||||
className={inputClass (false)}
|
||||
value={limit}
|
||||
onChange={ev => updateListQuery ({
|
||||
limit: Number (ev.target.value) as 20 | 50 | 100,
|
||||
})}>
|
||||
{LIST_LIMIT_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>
|
||||
{value} 件
|
||||
</option>))}
|
||||
</select>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="並び順">
|
||||
{() => (
|
||||
<select
|
||||
className={inputClass (false)}
|
||||
value={order}
|
||||
onChange={ev => updateListQuery ({
|
||||
order: ev.target.value as FetchPostsOrder,
|
||||
})}>
|
||||
{POST_LIST_ORDER_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>
|
||||
{postOrderLabel[value]}
|
||||
</option>))}
|
||||
</select>)}
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{/* タイトル */}
|
||||
<FormField label="タイトル">
|
||||
{({ invalid }) => (
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
getClientListOrder,
|
||||
LIST_LIMIT_OPTIONS,
|
||||
setClientListSettings,
|
||||
TAG_LIST_ORDER_OPTIONS,
|
||||
} from '@/lib/settings'
|
||||
import { fetchTags } from '@/lib/tags'
|
||||
import { dateString, inputClass } from '@/lib/utils'
|
||||
@@ -44,28 +43,30 @@ const boolFromQuery = (value: string | null): boolean =>
|
||||
|
||||
const tagStateLabel = (deprecatedAt: string | null) => deprecatedAt ? '廃止' : ''
|
||||
|
||||
const tagOrderLabel: Record<FetchTagsOrder, string> = {
|
||||
'name:asc': '名前昇順',
|
||||
'name:desc': '名前降順',
|
||||
'category:asc': 'カテゴリ昇順',
|
||||
'category:desc': 'カテゴリ降順',
|
||||
'post_count:asc': '件数昇順',
|
||||
'post_count:desc': '件数降順',
|
||||
'created_at:asc': '最初の記載日時 昇順',
|
||||
'created_at:desc': '最初の記載日時 降順',
|
||||
'updated_at:asc': '更新日時 昇順',
|
||||
'updated_at:desc': '更新日時 降順',
|
||||
}
|
||||
|
||||
const parseLimit = (value: string | null): 20 | 50 | 100 | null => {
|
||||
const n = Number (value)
|
||||
return n === 20 || n === 50 || n === 100 ? n : null
|
||||
}
|
||||
|
||||
const parseOrder = (value: string | null): FetchTagsOrder | null =>
|
||||
TAG_LIST_ORDER_OPTIONS.some (option => option === value)
|
||||
? value as FetchTagsOrder
|
||||
: null
|
||||
const parseOrder = (value: string | null): FetchTagsOrder | null => {
|
||||
switch (value)
|
||||
{
|
||||
case 'name:asc':
|
||||
case 'name:desc':
|
||||
case 'category:asc':
|
||||
case 'category:desc':
|
||||
case 'post_count:asc':
|
||||
case 'post_count:desc':
|
||||
case 'created_at:asc':
|
||||
case 'created_at:desc':
|
||||
case 'updated_at:asc':
|
||||
case 'updated_at:desc':
|
||||
return value
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const TagListPage: FC = () => {
|
||||
@@ -192,41 +193,26 @@ const TagListPage: FC = () => {
|
||||
</Helmet>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<PageTitle>タグ</PageTitle>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<PageTitle>タグ</PageTitle>
|
||||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>件数</span>
|
||||
<select
|
||||
value={limit}
|
||||
onChange={ev => updateListQuery ({
|
||||
limit: Number (ev.target.value) as 20 | 50 | 100,
|
||||
})}
|
||||
className="rounded border border-border bg-background px-2 py-1 text-sm
|
||||
text-foreground">
|
||||
{LIST_LIMIT_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch} className="space-y-2">
|
||||
<div className="grid gap-3 rounded-xl border border-border bg-background/80 p-3 sm:grid-cols-2">
|
||||
<FormField label="表示件数">
|
||||
{() => (
|
||||
<select
|
||||
value={limit}
|
||||
onChange={ev => updateListQuery ({
|
||||
limit: Number (ev.target.value) as 20 | 50 | 100,
|
||||
})}
|
||||
className={inputClass (false)}>
|
||||
{LIST_LIMIT_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>
|
||||
{value} 件
|
||||
</option>))}
|
||||
</select>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="並び順">
|
||||
{() => (
|
||||
<select
|
||||
value={order}
|
||||
onChange={ev => updateListQuery ({
|
||||
order: ev.target.value as FetchTagsOrder,
|
||||
})}
|
||||
className={inputClass (false)}>
|
||||
{TAG_LIST_ORDER_OPTIONS.map (value => (
|
||||
<option key={value} value={value}>
|
||||
{tagOrderLabel[value]}
|
||||
</option>))}
|
||||
</select>)}
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{/* 名前 */}
|
||||
<FormField label="名前">
|
||||
{({ invalid }) => (
|
||||
|
||||
ファイル差分が大きすぎるため省略します
差分を読込み
@@ -8,7 +8,6 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { useUserSettings } from '@/components/users/UserSettingsProvider'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiGet, apiPut } from '@/lib/api'
|
||||
@@ -25,27 +24,11 @@ import type { FC } from 'react'
|
||||
import type { User, WikiPage } from '@/types'
|
||||
|
||||
const mdParser = new MarkdownIt
|
||||
const FIXED_WIKI_EDITOR_VIEW = { menu: true, md: true, html: true } as const
|
||||
|
||||
type EditorView = {
|
||||
menu: boolean
|
||||
md: boolean
|
||||
html: boolean }
|
||||
|
||||
const editorView = (
|
||||
mode: 'split' | 'write' | 'preview',
|
||||
): EditorView => {
|
||||
switch (mode)
|
||||
{
|
||||
case 'write':
|
||||
return { menu: true, md: true, html: false }
|
||||
|
||||
case 'preview':
|
||||
return { menu: true, md: false, html: true }
|
||||
|
||||
default:
|
||||
return { menu: true, md: true, html: true }
|
||||
}
|
||||
}
|
||||
// `wiki_editor_mode` is still kept in backend settings, but the common
|
||||
// settings screen does not expose it at present. Until that UI returns, the
|
||||
// editor stays on the fixed split view here.
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
@@ -54,7 +37,6 @@ type WikiFormField = 'title' | 'body'
|
||||
|
||||
const WikiEditPage: FC<Props> = ({ user }) => {
|
||||
const editable = canEditContent (user)
|
||||
const { settings } = useUserSettings ()
|
||||
|
||||
const { id } = useParams ()
|
||||
|
||||
@@ -137,7 +119,7 @@ const WikiEditPage: FC<Props> = ({ user }) => {
|
||||
{() => (
|
||||
<MdEditor value={body}
|
||||
style={{ height: '500px' }}
|
||||
config={{ view: editorView (settings.wikiEditorMode) }}
|
||||
config={{ view: FIXED_WIKI_EDITOR_VIEW }}
|
||||
renderHTML={text => mdParser.render (text)}
|
||||
onChange={({ text }) => setBody (text)}/>)}
|
||||
</FormField>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
import FormField from '@/components/common/FormField'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { useUserSettings } from '@/components/users/UserSettingsProvider'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { apiPost } from '@/lib/api'
|
||||
@@ -23,27 +22,11 @@ import 'react-markdown-editor-lite/lib/index.css'
|
||||
import type { User, WikiPage } from '@/types'
|
||||
|
||||
const mdParser = new MarkdownIt
|
||||
const FIXED_WIKI_EDITOR_VIEW = { menu: true, md: true, html: true } as const
|
||||
|
||||
type EditorView = {
|
||||
menu: boolean
|
||||
md: boolean
|
||||
html: boolean }
|
||||
|
||||
const editorView = (
|
||||
mode: 'split' | 'write' | 'preview',
|
||||
): EditorView => {
|
||||
switch (mode)
|
||||
{
|
||||
case 'write':
|
||||
return { menu: true, md: true, html: false }
|
||||
|
||||
case 'preview':
|
||||
return { menu: true, md: false, html: true }
|
||||
|
||||
default:
|
||||
return { menu: true, md: true, html: true }
|
||||
}
|
||||
}
|
||||
// `wiki_editor_mode` is still kept in backend settings, but the common
|
||||
// settings screen does not expose it at present. Until that UI returns, the
|
||||
// editor stays on the fixed split view here.
|
||||
|
||||
type Props = { user: User | null }
|
||||
|
||||
@@ -52,7 +35,6 @@ type WikiFormField = 'title' | 'body'
|
||||
|
||||
const WikiNewPage: FC<Props> = ({ user }) => {
|
||||
const editable = canEditContent (user)
|
||||
const { settings } = useUserSettings ()
|
||||
|
||||
const location = useLocation ()
|
||||
const navigate = useNavigate ()
|
||||
@@ -115,7 +97,7 @@ const WikiNewPage: FC<Props> = ({ user }) => {
|
||||
{() => (
|
||||
<MdEditor value={body}
|
||||
style={{ height: '500px' }}
|
||||
config={{ view: editorView (settings.wikiEditorMode) }}
|
||||
config={{ view: FIXED_WIKI_EDITOR_VIEW }}
|
||||
renderHTML={text => mdParser.render (text)}
|
||||
onChange={({ text }) => setBody (text)}/>)}
|
||||
</FormField>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする