This commit is contained in:
@@ -9,6 +9,19 @@ class PostsController < ApplicationController
|
||||
created_between = params[:created_from].presence, params[:created_to].presence
|
||||
updated_between = params[:updated_from].presence, params[:updated_to].presence
|
||||
|
||||
order = params[:order].to_s.split(':', 2).map(&:strip)
|
||||
unless order[0].in?(['title', 'url', 'original_created_at', 'created_at', 'updated_at'])
|
||||
order[0] = 'original_created_at'
|
||||
end
|
||||
unless order[1].in?(['asc', 'desc'])
|
||||
order[1] =
|
||||
if order[0].in?(['title', 'url'])
|
||||
'asc'
|
||||
else
|
||||
'desc'
|
||||
end
|
||||
end
|
||||
|
||||
page = (params[:page].presence || 1).to_i
|
||||
limit = (params[:limit].presence || 20).to_i
|
||||
|
||||
@@ -36,11 +49,16 @@ class PostsController < ApplicationController
|
||||
q = q.where('posts.updated_at <= ?', updated_between[1]) if updated_between[1]
|
||||
|
||||
sort_sql =
|
||||
if order[0] == 'original_created_at'
|
||||
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE,' +
|
||||
'posts.original_created_from,' +
|
||||
'posts.created_at)'
|
||||
posts = q.select("posts.*, #{ sort_sql } AS sort_ts")
|
||||
.order(Arel.sql("#{ sort_sql } DESC"))
|
||||
'posts.created_at) ' +
|
||||
order[1]
|
||||
else
|
||||
"posts.#{ order[0] } #{ order[1] }"
|
||||
end
|
||||
posts = q.select('posts.*')
|
||||
.order(Arel.sql("#{ sort_sql }"))
|
||||
.limit(limit).offset(offset).to_a
|
||||
|
||||
render json: { posts: posts.map { |post|
|
||||
|
||||
@@ -8,6 +8,8 @@ import { fetchWikiPage,
|
||||
fetchWikiPageByTitle,
|
||||
fetchWikiPages } from '@/lib/wiki'
|
||||
|
||||
import type { FetchPostsOrder } from '@/types'
|
||||
|
||||
type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
|
||||
|
||||
const mPost = match<{ id: string }> ('/posts/:id')
|
||||
@@ -79,17 +81,19 @@ const prefetchPostsIndex: Prefetcher = async (qc, url) => {
|
||||
const m: 'all' | 'any' = url.searchParams.get ('match') === 'any' ? 'any' : 'all'
|
||||
const page = Number (url.searchParams.get ('page') || 1)
|
||||
const limit = Number (url.searchParams.get ('limit') || 20)
|
||||
const order = url.searchParams.get ('order') as FetchPostsOrder | null
|
||||
|
||||
const keys = {
|
||||
tags, match: m, page, limit,
|
||||
...(qURL && { url: qURL }),
|
||||
...(title && { title }),
|
||||
...(originalCreatedFrom && { original_created_from: originalCreatedFrom }),
|
||||
...(originalCreatedTo && { original_created_to: originalCreatedTo }),
|
||||
...(createdFrom && { created_from: createdFrom }),
|
||||
...(createdTo && { created_to: createdTo }),
|
||||
...(updatedFrom && { updated_from: updatedFrom }),
|
||||
...(updatedTo && { updated_to: updatedTo }) }
|
||||
...(originalCreatedFrom && { originalCreatedFrom }),
|
||||
...(originalCreatedTo && { originalCreatedTo }),
|
||||
...(createdFrom && { createdFrom }),
|
||||
...(createdTo && { createdTo }),
|
||||
...(updatedFrom && { updatedFrom }),
|
||||
...(updatedTo && { updatedTo }),
|
||||
...(order && { order }) }
|
||||
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: postsKeys.index (keys),
|
||||
|
||||
@@ -18,7 +18,10 @@ import { postsKeys } from '@/lib/queryKeys'
|
||||
|
||||
import type { FC, ChangeEvent, FormEvent, KeyboardEvent } from 'react'
|
||||
|
||||
import type { FetchPostsOrder, FetchPostsParams, Tag } from '@/types'
|
||||
import type { FetchPostsOrder,
|
||||
FetchPostsOrderField,
|
||||
FetchPostsParams,
|
||||
Tag } from '@/types'
|
||||
|
||||
|
||||
const setIf = (qs: URLSearchParams, k: string, v: string | null) => {
|
||||
@@ -49,13 +52,12 @@ export default (() => {
|
||||
const qCreatedTo = query.get ('created_to')
|
||||
const qUpdatedFrom = query.get ('updated_from')
|
||||
const qUpdatedTo = query.get ('updated_to')
|
||||
const qOrder = (query.get ('order') || 'original_created_at:desc') as FetchPostsOrder
|
||||
const order = (query.get ('order') || 'original_created_at:desc') as FetchPostsOrder
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState (-1)
|
||||
const [createdFrom, setCreatedFrom] = useState (qCreatedFrom)
|
||||
const [createdTo, setCreatedTo] = useState (qCreatedTo)
|
||||
const [matchType, setMatchType] = useState (qMatch ?? 'all')
|
||||
const [order, setOrder] = useState<FetchPostsOrder> ('original_created_at:desc')
|
||||
const [originalCreatedFrom, setOriginalCreatedFrom] = useState (qOriginalCreatedFrom)
|
||||
const [originalCreatedTo, setOriginalCreatedTo] = useState (qOriginalCreatedTo)
|
||||
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
||||
@@ -76,7 +78,7 @@ export default (() => {
|
||||
...(qCreatedTo && { createdTo: qCreatedTo }),
|
||||
...(qUpdatedFrom && { updatedFrom: qUpdatedFrom }),
|
||||
...(qUpdatedTo && { updatedTo: qUpdatedTo }),
|
||||
...(qOrder && { order: qOrder }) }
|
||||
...(order && { order }) }
|
||||
const { data, isLoading: loading } = useQuery ({
|
||||
queryKey: postsKeys.index (keys),
|
||||
queryFn: () => fetchPosts (keys) })
|
||||
@@ -94,11 +96,32 @@ export default (() => {
|
||||
setCreatedTo (qCreatedTo)
|
||||
setUpdatedFrom (qUpdatedFrom)
|
||||
setUpdatedTo (qUpdatedTo)
|
||||
setOrder (qOrder)
|
||||
|
||||
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
|
||||
}, [location.search])
|
||||
|
||||
const SortHeader = ({ by, label }: { by: FetchPostsOrderField; label: string }) => {
|
||||
const [fld, dir] = order.split (':')
|
||||
|
||||
const qs = new URLSearchParams (location.search)
|
||||
const nextDir =
|
||||
(by === fld)
|
||||
? (dir === 'asc' ? 'desc' : 'asc')
|
||||
: (['title', 'url'].includes (by) ? 'asc' : 'desc')
|
||||
qs.set ('order', `${ by }:${ nextDir }`)
|
||||
qs.set ('page', '1')
|
||||
|
||||
return (
|
||||
<PrefetchLink
|
||||
className="text-inherit visited:text-inherit hover:text-inherit"
|
||||
to={`${ location.pathname }?${ qs.toString () }`}>
|
||||
<span className="font-bold">
|
||||
{label}
|
||||
{by === fld && (dir === 'asc' ? ' ▲' : ' ▼')}
|
||||
</span>
|
||||
</PrefetchLink>)
|
||||
}
|
||||
|
||||
// TODO: TagSearch からのコピペのため,共通化を考へる.
|
||||
const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
setTagsStr (ev.target.value)
|
||||
@@ -305,7 +328,7 @@ export default (() => {
|
||||
<col className="w-72"/>
|
||||
<col className="w-80"/>
|
||||
<col className="w-[24rem]"/>
|
||||
<col className="w-44"/>
|
||||
<col className="w-60"/>
|
||||
<col className="w-44"/>
|
||||
<col className="w-44"/>
|
||||
</colgroup>
|
||||
@@ -313,12 +336,22 @@ export default (() => {
|
||||
<thead className="border-b-2 border-black dark:border-white">
|
||||
<tr>
|
||||
<th className="p-2 text-left whitespace-nowrap">投稿</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">タイトル</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">URL</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="title" label="タイトル"/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="url" label="URL"/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">タグ</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">オリジナルの投稿日時</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">投稿日時</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">更新日時</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="original_created_at" label="オリジナルの投稿日時"/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="created_at" label="投稿日時"/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader by="updated_at" label="更新日時"/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
Reference in New Issue
Block a user