This commit is contained in:
2026-03-01 15:05:40 +09:00
parent a088503272
commit 1350ae3d99
3 changed files with 77 additions and 22 deletions
+23 -5
View File
@@ -9,6 +9,19 @@ class PostsController < ApplicationController
created_between = params[:created_from].presence, params[:created_to].presence created_between = params[:created_from].presence, params[:created_to].presence
updated_between = params[:updated_from].presence, params[:updated_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 page = (params[:page].presence || 1).to_i
limit = (params[:limit].presence || 20).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] q = q.where('posts.updated_at <= ?', updated_between[1]) if updated_between[1]
sort_sql = sort_sql =
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE,' + if order[0] == 'original_created_at'
'posts.original_created_from,' + 'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE,' +
'posts.created_at)' 'posts.original_created_from,' +
posts = q.select("posts.*, #{ sort_sql } AS sort_ts") 'posts.created_at) ' +
.order(Arel.sql("#{ sort_sql } DESC")) order[1]
else
"posts.#{ order[0] } #{ order[1] }"
end
posts = q.select('posts.*')
.order(Arel.sql("#{ sort_sql }"))
.limit(limit).offset(offset).to_a .limit(limit).offset(offset).to_a
render json: { posts: posts.map { |post| render json: { posts: posts.map { |post|
+10 -6
View File
@@ -8,6 +8,8 @@ import { fetchWikiPage,
fetchWikiPageByTitle, fetchWikiPageByTitle,
fetchWikiPages } from '@/lib/wiki' fetchWikiPages } from '@/lib/wiki'
import type { FetchPostsOrder } from '@/types'
type Prefetcher = (qc: QueryClient, url: URL) => Promise<void> type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
const mPost = match<{ id: string }> ('/posts/:id') 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 m: 'all' | 'any' = url.searchParams.get ('match') === 'any' ? 'any' : 'all'
const page = Number (url.searchParams.get ('page') || 1) const page = Number (url.searchParams.get ('page') || 1)
const limit = Number (url.searchParams.get ('limit') || 20) const limit = Number (url.searchParams.get ('limit') || 20)
const order = url.searchParams.get ('order') as FetchPostsOrder | null
const keys = { const keys = {
tags, match: m, page, limit, tags, match: m, page, limit,
...(qURL && { url: qURL }), ...(qURL && { url: qURL }),
...(title && { title }), ...(title && { title }),
...(originalCreatedFrom && { original_created_from: originalCreatedFrom }), ...(originalCreatedFrom && { originalCreatedFrom }),
...(originalCreatedTo && { original_created_to: originalCreatedTo }), ...(originalCreatedTo && { originalCreatedTo }),
...(createdFrom && { created_from: createdFrom }), ...(createdFrom && { createdFrom }),
...(createdTo && { created_to: createdTo }), ...(createdTo && { createdTo }),
...(updatedFrom && { updated_from: updatedFrom }), ...(updatedFrom && { updatedFrom }),
...(updatedTo && { updated_to: updatedTo }) } ...(updatedTo && { updatedTo }),
...(order && { order }) }
await qc.prefetchQuery ({ await qc.prefetchQuery ({
queryKey: postsKeys.index (keys), queryKey: postsKeys.index (keys),
+44 -11
View File
@@ -18,7 +18,10 @@ import { postsKeys } from '@/lib/queryKeys'
import type { FC, ChangeEvent, FormEvent, KeyboardEvent } from 'react' 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) => { const setIf = (qs: URLSearchParams, k: string, v: string | null) => {
@@ -49,13 +52,12 @@ export default (() => {
const qCreatedTo = query.get ('created_to') const qCreatedTo = query.get ('created_to')
const qUpdatedFrom = query.get ('updated_from') const qUpdatedFrom = query.get ('updated_from')
const qUpdatedTo = query.get ('updated_to') 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 [activeIndex, setActiveIndex] = useState (-1)
const [createdFrom, setCreatedFrom] = useState (qCreatedFrom) const [createdFrom, setCreatedFrom] = useState (qCreatedFrom)
const [createdTo, setCreatedTo] = useState (qCreatedTo) const [createdTo, setCreatedTo] = useState (qCreatedTo)
const [matchType, setMatchType] = useState (qMatch ?? 'all') const [matchType, setMatchType] = useState (qMatch ?? 'all')
const [order, setOrder] = useState<FetchPostsOrder> ('original_created_at:desc')
const [originalCreatedFrom, setOriginalCreatedFrom] = useState (qOriginalCreatedFrom) const [originalCreatedFrom, setOriginalCreatedFrom] = useState (qOriginalCreatedFrom)
const [originalCreatedTo, setOriginalCreatedTo] = useState (qOriginalCreatedTo) const [originalCreatedTo, setOriginalCreatedTo] = useState (qOriginalCreatedTo)
const [suggestions, setSuggestions] = useState<Tag[]> ([]) const [suggestions, setSuggestions] = useState<Tag[]> ([])
@@ -76,7 +78,7 @@ export default (() => {
...(qCreatedTo && { createdTo: qCreatedTo }), ...(qCreatedTo && { createdTo: qCreatedTo }),
...(qUpdatedFrom && { updatedFrom: qUpdatedFrom }), ...(qUpdatedFrom && { updatedFrom: qUpdatedFrom }),
...(qUpdatedTo && { updatedTo: qUpdatedTo }), ...(qUpdatedTo && { updatedTo: qUpdatedTo }),
...(qOrder && { order: qOrder }) } ...(order && { order }) }
const { data, isLoading: loading } = useQuery ({ const { data, isLoading: loading } = useQuery ({
queryKey: postsKeys.index (keys), queryKey: postsKeys.index (keys),
queryFn: () => fetchPosts (keys) }) queryFn: () => fetchPosts (keys) })
@@ -94,11 +96,32 @@ export default (() => {
setCreatedTo (qCreatedTo) setCreatedTo (qCreatedTo)
setUpdatedFrom (qUpdatedFrom) setUpdatedFrom (qUpdatedFrom)
setUpdatedTo (qUpdatedTo) setUpdatedTo (qUpdatedTo)
setOrder (qOrder)
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' }) document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
}, [location.search]) }, [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 からのコピペのため,共通化を考へる. // TODO: TagSearch からのコピペのため,共通化を考へる.
const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => { const whenChanged = async (ev: ChangeEvent<HTMLInputElement>) => {
setTagsStr (ev.target.value) setTagsStr (ev.target.value)
@@ -305,7 +328,7 @@ export default (() => {
<col className="w-72"/> <col className="w-72"/>
<col className="w-80"/> <col className="w-80"/>
<col className="w-[24rem]"/> <col className="w-[24rem]"/>
<col className="w-44"/> <col className="w-60"/>
<col className="w-44"/> <col className="w-44"/>
<col className="w-44"/> <col className="w-44"/>
</colgroup> </colgroup>
@@ -313,12 +336,22 @@ export default (() => {
<thead className="border-b-2 border-black dark:border-white"> <thead className="border-b-2 border-black dark:border-white">
<tr> <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"></th> <th className="p-2 text-left whitespace-nowrap">
<th className="p-2 text-left whitespace-nowrap">URL</th> <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 className="p-2 text-left whitespace-nowrap">稿</th> <SortHeader by="original_created_at" label="オリジナルの投稿日時"/>
<th className="p-2 text-left whitespace-nowrap"></th> </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> </tr>
</thead> </thead>
<tbody> <tbody>