#318 #318 #318 #318 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #328
This commit was merged in pull request #328.
This commit is contained in:
@@ -26,6 +26,7 @@ import PostNewPage from '@/pages/posts/PostNewPage'
|
||||
import PostSearchPage from '@/pages/posts/PostSearchPage'
|
||||
import ServiceUnavailable from '@/pages/ServiceUnavailable'
|
||||
import SettingPage from '@/pages/users/SettingPage'
|
||||
import TagDetailPage from '@/pages/tags/TagDetailPage'
|
||||
import TagListPage from '@/pages/tags/TagListPage'
|
||||
import TheatreDetailPage from '@/pages/theatres/TheatreDetailPage'
|
||||
import WikiDetailPage from '@/pages/wiki/WikiDetailPage'
|
||||
@@ -55,6 +56,7 @@ const RouteTransitionWrapper = ({ user, setUser }: {
|
||||
<Route path="/posts/:id" element={<PostDetailRoute user={user}/>}/>
|
||||
<Route path="/posts/changes" element={<PostHistoryPage/>}/>
|
||||
<Route path="/tags" element={<TagListPage/>}/>
|
||||
<Route path="/tags/:id" element={<TagDetailPage/>}/>
|
||||
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
|
||||
<Route path="/theatres/:id" element={<TheatreDetailPage/>}/>
|
||||
<Route path="/materials" element={<MaterialBasePage/>}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import PrefetchLink from '@/components/PrefetchLink'
|
||||
import TopNavUser from '@/components/TopNavUser'
|
||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||
import { tagsKeys, wikiKeys } from '@/lib/queryKeys'
|
||||
import { fetchTagByName } from '@/lib/tags'
|
||||
import { fetchTag, fetchTagByName } from '@/lib/tags'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { fetchWikiPage } from '@/lib/wiki'
|
||||
|
||||
@@ -29,6 +29,8 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
|
||||
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (pathName) && wikiId)
|
||||
const wikiTitle = pathName.split ('/')[2] ?? ''
|
||||
|
||||
const tagFlg = /^\/tags\/\d+/.test (pathName)
|
||||
|
||||
return [
|
||||
{ name: '広場', to: '/posts', subMenu: [
|
||||
{ name: '一覧', to: '/posts' },
|
||||
@@ -38,10 +40,13 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
|
||||
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
|
||||
{ name: 'タグ', to: '/tags', subMenu: [
|
||||
{ name: 'マスタ', to: '/tags' },
|
||||
{ name: '別名タグ', to: '/tags/aliases', visible: false },
|
||||
{ name: '上位タグ', to: '/tags/implications', visible: false },
|
||||
{ name: 'ニコニコ連携', to: '/tags/nico' },
|
||||
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }] },
|
||||
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' },
|
||||
{ component: <Separator/>, visible: tagFlg },
|
||||
{ name: `広場 (${ postCount || 0 })`,
|
||||
to: `/posts?tags=${ encodeURIComponent (tag?.name ?? '') }`,
|
||||
visible: tagFlg },
|
||||
{ name: '履歴', to: `/tags/changes?id=${ tag?.id }`, visible: false }] },
|
||||
{ name: '素材', to: '/materials', visible: false, subMenu: [
|
||||
{ name: '一覧', to: '/materials' },
|
||||
{ name: '検索', to: '/materials/search', visible: false },
|
||||
@@ -114,12 +119,14 @@ export default (({ user }: Props) => {
|
||||
queryKey: wikiKeys.show (wikiIdStr, { }),
|
||||
queryFn: () => fetchWikiPage (wikiIdStr, { }) })
|
||||
|
||||
const effectiveTitle = wikiPage?.title ?? ''
|
||||
const tagFlg = /^\/tags\/\d+/.test (location.pathname)
|
||||
const effectiveTitle = (tagFlg ? location.pathname.split ('/')[2] : wikiPage?.title) ?? ''
|
||||
|
||||
const { data: tag } = useQuery ({
|
||||
enabled: Boolean (effectiveTitle),
|
||||
queryKey: tagsKeys.show (effectiveTitle),
|
||||
queryFn: () => fetchTagByName (effectiveTitle) })
|
||||
queryFn: () => (tagFlg ? fetchTag : fetchTagByName) (effectiveTitle) })
|
||||
|
||||
|
||||
const menu = menuOutline ({ tag, wikiId, user, pathName: location.pathname })
|
||||
const visibleMenu = menu.filter ((item): item is MenuVisibleItem => item.visible ?? true)
|
||||
|
||||
@@ -14,6 +14,7 @@ type Prefetcher = (qc: QueryClient, url: URL) => Promise<void>
|
||||
|
||||
const mPost = match<{ id: string }> ('/posts/:id')
|
||||
const mWiki = match<{ title: string }> ('/wiki/:title')
|
||||
const mTag = match<{ id: string }> ('/tags/:id')
|
||||
|
||||
|
||||
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
|
||||
@@ -169,6 +170,19 @@ const prefetchTagsIndex: Prefetcher = async (qc, url) => {
|
||||
}
|
||||
|
||||
|
||||
const prefetchTagShow: Prefetcher = async (qc, url) => {
|
||||
const m = mTag (url.pathname)
|
||||
if (!(m))
|
||||
return
|
||||
|
||||
const { id } = m.params
|
||||
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: tagsKeys.show (id),
|
||||
queryFn: () => fetchTag (id) })
|
||||
}
|
||||
|
||||
|
||||
export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[] = [
|
||||
{ test: u => ['/', '/posts', '/posts/search'].includes (u.pathname),
|
||||
run: prefetchPostsIndex },
|
||||
@@ -180,7 +194,9 @@ export const routePrefetchers: { test: (u: URL) => boolean; run: Prefetcher }[]
|
||||
{ test: u => (!(['/wiki/new', '/wiki/changes'].includes (u.pathname))
|
||||
&& Boolean (mWiki (u.pathname))),
|
||||
run: prefetchWikiPageShow },
|
||||
{ test: u => u.pathname === '/tags', run: prefetchTagsIndex }]
|
||||
{ test: u => u.pathname === '/tags', run: prefetchTagsIndex },
|
||||
{ test: u => u.pathname !== '/tags/nico' && Boolean (mTag (u.pathname)),
|
||||
run: prefetchTagShow }]
|
||||
|
||||
|
||||
export const prefetchForURL = async (qc: QueryClient, urlLike: string): Promise<void> => {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import TagLink from '@/components/TagLink'
|
||||
import Label from '@/components/common/Label'
|
||||
import PageTitle from '@/components/common/PageTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
|
||||
import { apiPut } from '@/lib/api'
|
||||
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
|
||||
import { fetchTag } from '@/lib/tags'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { FC, FormEvent } from 'react'
|
||||
|
||||
import type { Category, Tag } from '@/types'
|
||||
|
||||
|
||||
export default (() => {
|
||||
const { id } = useParams ()
|
||||
const tagId = String (id ?? '')
|
||||
const tagKey = tagsKeys.show (tagId)
|
||||
|
||||
const { data: tag, isLoading: loading } = useQuery ({
|
||||
queryKey: tagKey,
|
||||
queryFn: () => fetchTag (tagId) })
|
||||
|
||||
const [name, setName] = useState ('')
|
||||
const [category, setCategory] = useState<Category> ('general')
|
||||
const [aliases, setAliases] = useState ('')
|
||||
const [parentTags, setParentTags] = useState ('')
|
||||
const [disabled, setDisabled] = useState (true)
|
||||
|
||||
const qc = useQueryClient ()
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault ()
|
||||
|
||||
const formData = new FormData
|
||||
formData.append ('name', name)
|
||||
formData.append ('category', category)
|
||||
formData.append ('aliases', aliases)
|
||||
formData.append ('parent_tags', parentTags)
|
||||
|
||||
try
|
||||
{
|
||||
const data = await apiPut<Tag> (`/tags/${ id }`, formData)
|
||||
|
||||
setName (data.name)
|
||||
setCategory (data.category as Category)
|
||||
setAliases (data.aliases.join (' '))
|
||||
setParentTags (data.parents.map (t => t.name).join (' '))
|
||||
|
||||
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
||||
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
||||
toast ({ description: '更新しました.' })
|
||||
}
|
||||
catch
|
||||
{
|
||||
toast ({ description: '更新に失敗しました.' })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect (() => {
|
||||
if (!(tag))
|
||||
{
|
||||
setDisabled (true)
|
||||
return
|
||||
}
|
||||
|
||||
setName (tag.name)
|
||||
setCategory (tag.category as Category)
|
||||
setAliases (tag.aliases.join (' '))
|
||||
setParentTags (tag.parents.map (t => t.name).join (' '))
|
||||
setDisabled (tag.category === 'nico')
|
||||
}, [tag])
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
{(loading || !(tag)) ? 'Loading...' : (
|
||||
<div className="max-w-xl">
|
||||
<PageTitle>
|
||||
<TagLink
|
||||
tag={tag}
|
||||
withWiki={false}
|
||||
withCount={false}/>
|
||||
</PageTitle>
|
||||
|
||||
<form onSubmit={handleSubmit} className="my-4 space-y-2">
|
||||
{/* 名称 */}
|
||||
<div>
|
||||
<Label>名称</Label>
|
||||
{/* TODO: 補完に対応させる */}
|
||||
<input
|
||||
type="text"
|
||||
disabled={disabled}
|
||||
value={name}
|
||||
onChange={e => setName (e.target.value)}
|
||||
className="w-full border p-2 rounded"/>
|
||||
</div>
|
||||
|
||||
{/* カテゴリ */}
|
||||
<div>
|
||||
<Label>カテゴリ</Label>
|
||||
<select
|
||||
disabled={disabled}
|
||||
value={category ?? ''}
|
||||
onChange={e => setCategory(e.target.value as Category)}
|
||||
className="w-full border p-2 rounded">
|
||||
{CATEGORIES.filter (cat => tag.category === 'nico' || cat !== 'nico')
|
||||
.map (cat => (
|
||||
<option key={cat} value={cat}>
|
||||
{CATEGORY_NAMES[cat]}
|
||||
</option>))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 別名 */}
|
||||
<div>
|
||||
<Label>別名</Label>
|
||||
{/* TODO: 補完に対応させる */}
|
||||
<input
|
||||
type="text"
|
||||
disabled={disabled}
|
||||
value={aliases}
|
||||
onChange={e => setAliases (e.target.value)}
|
||||
className="w-full border p-2 rounded"/>
|
||||
</div>
|
||||
|
||||
{/* 上位タグ */}
|
||||
<div>
|
||||
<Label>上位タグ</Label>
|
||||
{/* TODO: 補完に対応させる */}
|
||||
<input
|
||||
type="text"
|
||||
disabled={disabled}
|
||||
value={parentTags}
|
||||
onChange={e => setParentTags (e.target.value)}
|
||||
className="w-full border p-2 rounded"/>
|
||||
</div>
|
||||
|
||||
<div className="py-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={disabled}
|
||||
className={cn ('px-4 py-2 rounded',
|
||||
(disabled
|
||||
? 'text-gray-300 bg-gray-500'
|
||||
: 'text-white bg-blue-500'))}>
|
||||
更新
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>)}
|
||||
</MainArea>)
|
||||
}) satisfies FC
|
||||
@@ -205,13 +205,15 @@ export default (() => {
|
||||
{loading ? 'Loading...' : (results.length > 0 ? (
|
||||
<div className="mt-4">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[1200px] table-fixed border-collapse">
|
||||
<table className="w-full min-w-[2000px] table-fixed border-collapse">
|
||||
<colgroup>
|
||||
<col className="w-72"/>
|
||||
<col className="w-48"/>
|
||||
<col className="w-16"/>
|
||||
<col className="w-44"/>
|
||||
<col className="w-44"/>
|
||||
<col className="w-48"/>
|
||||
<col className="w-72"/>
|
||||
<col className="w-48"/>
|
||||
<col className="w-56"/>
|
||||
<col className="w-56"/>
|
||||
<col className="w-16"/>
|
||||
</colgroup>
|
||||
|
||||
@@ -224,13 +226,6 @@ export default (() => {
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="category"
|
||||
label="カテゴリ"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="post_count"
|
||||
@@ -238,6 +233,15 @@ export default (() => {
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
<SortHeader<FetchTagsOrderField>
|
||||
by="category"
|
||||
label="カテゴリ"
|
||||
currentOrder={order}
|
||||
defaultDirection={defaultDirection}/>
|
||||
</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<FetchTagsOrderField>
|
||||
by="created_at"
|
||||
@@ -260,10 +264,23 @@ export default (() => {
|
||||
{results.map (row => (
|
||||
<tr key={row.id} className="even:bg-gray-100 dark:even:bg-gray-700">
|
||||
<td className="p-2">
|
||||
<TagLink tag={row} withCount={false}/>
|
||||
<TagLink
|
||||
tag={row}
|
||||
to={`/tags/${ encodeURIComponent (row.id) }`}
|
||||
withCount={false}/>
|
||||
</td>
|
||||
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
|
||||
<td className="p-2 text-right">{row.postCount}</td>
|
||||
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
|
||||
<td className="p-2">{row.aliases.join (' ')}</td>
|
||||
<td className="p-2">
|
||||
{row.parents.map (t => (
|
||||
<span key={t.id} className="mr-2">
|
||||
<TagLink
|
||||
tag={t}
|
||||
withWiki={false}
|
||||
withCount={false}/>
|
||||
</span>))}
|
||||
</td>
|
||||
<td className="p-2">{dateString (row.createdAt)}</td>
|
||||
<td className="p-2">{dateString (row.updatedAt)}</td>
|
||||
<td className="p-2">
|
||||
|
||||
@@ -114,13 +114,15 @@ export default () => {
|
||||
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
|
||||
</h1>
|
||||
{loading ? <div>Loading...</div> : <WikiBody title={title} body={wikiPage?.body}/>}
|
||||
</article>
|
||||
|
||||
{(!(version) && posts.length > 0) && (
|
||||
<TabGroup>
|
||||
<Tab name="広場">
|
||||
<PostList posts={posts}/>
|
||||
</Tab>
|
||||
</TabGroup>)}
|
||||
{(!(version) && posts.length > 0) && (
|
||||
<div className="not-prose">
|
||||
<TabGroup>
|
||||
<Tab name="広場">
|
||||
<PostList posts={posts}/>
|
||||
</Tab>
|
||||
</TabGroup>
|
||||
</div>)}
|
||||
</article>
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
@@ -165,6 +165,8 @@ export type Tag = {
|
||||
id: number
|
||||
name: string
|
||||
category: Category
|
||||
aliases: string[]
|
||||
parents: Tag[]
|
||||
postCount: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
|
||||
Reference in New Issue
Block a user