このコミットが含まれているのは:
@@ -17,6 +17,10 @@ const mWiki = match<{ title: string }> ('/wiki/:title')
|
||||
const mTag = match<{ id: string }> ('/tags/:id')
|
||||
|
||||
|
||||
const boolFromQuery = (value: string | null): boolean =>
|
||||
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
|
||||
|
||||
|
||||
const prefetchWikiPagesIndex: Prefetcher = async (qc, url) => {
|
||||
const title = url.searchParams.get ('title') ?? ''
|
||||
|
||||
@@ -156,13 +160,16 @@ const prefetchTagsIndex: Prefetcher = async (qc, url) => {
|
||||
const createdTo = url.searchParams.get ('created_to') ?? ''
|
||||
const updatedFrom = url.searchParams.get ('updated_from') ?? ''
|
||||
const updatedTo = url.searchParams.get ('updated_to') ?? ''
|
||||
const deprecated = url.searchParams.has ('deprecated')
|
||||
? boolFromQuery (url.searchParams.get ('deprecated'))
|
||||
: null
|
||||
const page = Number (url.searchParams.get ('page') || 1)
|
||||
const limit = Number (url.searchParams.get ('limit') || 20)
|
||||
const order = (url.searchParams.get ('order') ?? 'post_count:desc') as FetchTagsOrder
|
||||
|
||||
const keys = {
|
||||
post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
|
||||
updatedFrom, updatedTo, page, limit, order }
|
||||
updatedFrom, updatedTo, deprecated, page, limit, order }
|
||||
|
||||
await qc.prefetchQuery ({
|
||||
queryKey: tagsKeys.index (keys),
|
||||
|
||||
@@ -20,6 +20,7 @@ const baseParams: FetchTagsParams = {
|
||||
createdTo: '',
|
||||
updatedFrom: '',
|
||||
updatedTo: '',
|
||||
deprecated: null,
|
||||
page: 1,
|
||||
limit: 30,
|
||||
order: 'updated_at:desc',
|
||||
|
||||
@@ -10,7 +10,8 @@ import type { Deerjikist,
|
||||
|
||||
export const fetchTags = async (
|
||||
{ post, name, category, postCountGTE, postCountLTE, createdFrom, createdTo,
|
||||
updatedFrom, updatedTo, page, limit, order }: FetchTagsParams,
|
||||
updatedFrom, updatedTo, deprecated,
|
||||
page, limit, order }: FetchTagsParams,
|
||||
): Promise<{ tags: Tag[]
|
||||
count: number }> =>
|
||||
await apiGet ('/tags', { params: {
|
||||
@@ -23,6 +24,7 @@ export const fetchTags = async (
|
||||
...(createdTo && { created_to: createdTo }),
|
||||
...(updatedFrom && { updated_from: updatedFrom }),
|
||||
...(updatedTo && { updated_to: updatedTo }),
|
||||
...(deprecated != null && { deprecated: deprecated ? '1' : '0' }),
|
||||
...(page && { page }),
|
||||
...(limit && { limit }),
|
||||
...(order && { order }) } })
|
||||
|
||||
@@ -19,7 +19,12 @@ import type { FC, FormEvent } from 'react'
|
||||
|
||||
import type { Category, Tag } from '@/types'
|
||||
|
||||
type TagFormField = 'name' | 'category' | 'aliases' | 'parentTags'
|
||||
type TagFormField =
|
||||
| 'name'
|
||||
| 'category'
|
||||
| 'aliases'
|
||||
| 'parentTags'
|
||||
| 'deprecated'
|
||||
|
||||
|
||||
const TagDetailPage: FC = () => {
|
||||
@@ -35,6 +40,7 @@ const TagDetailPage: FC = () => {
|
||||
const [category, setCategory] = useState<Category> ('general')
|
||||
const [aliases, setAliases] = useState ('')
|
||||
const [parentTags, setParentTags] = useState ('')
|
||||
const [deprecated, setDeprecated] = useState (false)
|
||||
const [disabled, setDisabled] = useState (true)
|
||||
const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
|
||||
useValidationErrors<TagFormField> ()
|
||||
@@ -50,6 +56,7 @@ const TagDetailPage: FC = () => {
|
||||
formData.append ('category', category)
|
||||
formData.append ('aliases', aliases)
|
||||
formData.append ('parent_tags', parentTags)
|
||||
formData.append ('deprecated', deprecated ? '1' : '0')
|
||||
|
||||
try
|
||||
{
|
||||
@@ -59,6 +66,7 @@ const TagDetailPage: FC = () => {
|
||||
setCategory (data.category as Category)
|
||||
setAliases (data.aliases.join (' '))
|
||||
setParentTags (data.parents.map (t => t.name).join (' '))
|
||||
setDeprecated (Boolean (data.deprecatedAt))
|
||||
|
||||
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
||||
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
||||
@@ -82,6 +90,7 @@ const TagDetailPage: FC = () => {
|
||||
setCategory (tag.category as Category)
|
||||
setAliases (tag.aliases.join (' '))
|
||||
setParentTags (tag.parents.map (t => t.name).join (' '))
|
||||
setDeprecated (Boolean (tag.deprecatedAt))
|
||||
setDisabled (tag.category === 'nico')
|
||||
}, [tag])
|
||||
|
||||
@@ -165,6 +174,17 @@ const TagDetailPage: FC = () => {
|
||||
</>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="廃止済" messages={fieldErrors.deprecated}>
|
||||
{({ describedBy, invalid }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
checked={deprecated}
|
||||
onChange={e => setDeprecated (e.target.checked)}
|
||||
aria-describedby={describedBy}
|
||||
aria-invalid={invalid}/>)}
|
||||
</FormField>
|
||||
|
||||
<div className="py-3">
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -20,17 +20,28 @@ import type { FC } from 'react'
|
||||
|
||||
const renderDiff = (diff: { current: string | null; prev: string | null }) => (
|
||||
<>
|
||||
{(diff.prev && diff.prev !== diff.current) && (
|
||||
{diff.prev !== diff.current
|
||||
? (
|
||||
<>
|
||||
<del className="text-red-600 dark:text-red-400">
|
||||
{diff.prev}
|
||||
{diff.prev && <>{diff.prev}<br/></>}
|
||||
</del>
|
||||
{diff.current && <br/>}
|
||||
</>)}
|
||||
{diff.current}
|
||||
<ins className="text-green-600 dark:text-green-400">
|
||||
{diff.current}
|
||||
</ins>
|
||||
</>)
|
||||
: diff.current}
|
||||
</>)
|
||||
|
||||
|
||||
const tagStateLabel = (deprecatedAt: string | null) => deprecatedAt ? '廃止' : ''
|
||||
|
||||
|
||||
const renderStateDiff = (diff: { current: string | null; prev: string | null }) =>
|
||||
renderDiff ({ current: tagStateLabel (diff.current),
|
||||
prev: tagStateLabel (diff.prev) })
|
||||
|
||||
|
||||
const TagHistoryPage: FC = () => {
|
||||
const location = useLocation ()
|
||||
const query = new URLSearchParams (location.search)
|
||||
@@ -72,6 +83,8 @@ const TagHistoryPage: FC = () => {
|
||||
<col className="w-96"/>
|
||||
{/* カテゴリ */}
|
||||
<col className="w-96"/>
|
||||
{/* 状態 */}
|
||||
<col className="w-32"/>
|
||||
{/* 別名 */}
|
||||
<col className="w-[48rem]"/>
|
||||
{/* 上位タグ */}
|
||||
@@ -87,6 +100,7 @@ const TagHistoryPage: FC = () => {
|
||||
<th className="p-2 text-left">版</th>
|
||||
<th className="p-2 text-left">名称</th>
|
||||
<th className="p-2 text-left">カテゴリ</th>
|
||||
<th className="p-2 text-left">状態</th>
|
||||
<th className="p-2 text-left">別名</th>
|
||||
<th className="p-2 text-left">上位タグ</th>
|
||||
<th className="p-2 text-left">更新日時</th>
|
||||
@@ -106,6 +120,9 @@ const TagHistoryPage: FC = () => {
|
||||
prev: (change.category.prev
|
||||
&& CATEGORY_NAMES[change.category.prev]) })}
|
||||
</td>
|
||||
<td className="p-2 break-all">
|
||||
{renderStateDiff (change.deprecatedAt)}
|
||||
</td>
|
||||
<td className="p-2">
|
||||
{change.aliases.map ((tag, i) => (
|
||||
tag.type === 'added'
|
||||
@@ -178,6 +195,7 @@ const TagHistoryPage: FC = () => {
|
||||
`/tags/${ change.tagId }`,
|
||||
{ name: change.name.current,
|
||||
category: change.category.current,
|
||||
deprecated: change.deprecatedAt.current ? '1' : '0',
|
||||
aliases:
|
||||
change.aliases
|
||||
.filter (t => t.type !== 'removed')
|
||||
@@ -211,4 +229,5 @@ const TagHistoryPage: FC = () => {
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
export default TagHistoryPage
|
||||
|
||||
export default TagHistoryPage
|
||||
|
||||
@@ -29,6 +29,13 @@ const setIf = (qs: URLSearchParams, k: string, v: string | null) => {
|
||||
}
|
||||
|
||||
|
||||
const boolFromQuery = (value: string | null): boolean =>
|
||||
['1', 'true', 'on', 'yes', ''].includes ((value ?? '').toLowerCase ())
|
||||
|
||||
|
||||
const tagStateLabel = (deprecatedAt: string | null) => deprecatedAt ? '廃止' : ''
|
||||
|
||||
|
||||
const TagListPage: FC = () => {
|
||||
const location = useLocation ()
|
||||
|
||||
@@ -48,6 +55,9 @@ const TagListPage: FC = () => {
|
||||
const qCreatedTo = query.get ('created_to') ?? ''
|
||||
const qUpdatedFrom = query.get ('updated_from') ?? ''
|
||||
const qUpdatedTo = query.get ('updated_to') ?? ''
|
||||
const qDeprecated = query.has ('deprecated')
|
||||
? boolFromQuery (query.get ('deprecated'))
|
||||
: null
|
||||
const order = (query.get ('order') || 'post_count:desc') as FetchTagsOrder
|
||||
|
||||
const [name, setName] = useState ('')
|
||||
@@ -58,6 +68,7 @@ const TagListPage: FC = () => {
|
||||
const [createdTo, setCreatedTo] = useState<string | null> (null)
|
||||
const [updatedFrom, setUpdatedFrom] = useState<string | null> (null)
|
||||
const [updatedTo, setUpdatedTo] = useState<string | null> (null)
|
||||
const [deprecated, setDeprecated] = useState<boolean | null> (null)
|
||||
|
||||
const keys = {
|
||||
page, limit, order,
|
||||
@@ -69,7 +80,8 @@ const TagListPage: FC = () => {
|
||||
createdFrom: qCreatedFrom,
|
||||
createdTo: qCreatedTo,
|
||||
updatedFrom: qUpdatedFrom,
|
||||
updatedTo: qUpdatedTo }
|
||||
updatedTo: qUpdatedTo,
|
||||
deprecated: qDeprecated }
|
||||
const { data, isLoading: loading } = useQuery ({
|
||||
queryKey: tagsKeys.index (keys),
|
||||
queryFn: () => fetchTags (keys) })
|
||||
@@ -85,10 +97,11 @@ const TagListPage: FC = () => {
|
||||
setCreatedTo (qCreatedTo)
|
||||
setUpdatedFrom (qUpdatedFrom)
|
||||
setUpdatedTo (qUpdatedTo)
|
||||
setDeprecated (qDeprecated)
|
||||
|
||||
document.querySelector ('table')?.scrollIntoView ({ behavior: 'smooth' })
|
||||
}, [location.search, qCategory, qCreatedFrom, qCreatedTo, qName, qPostCountGTE,
|
||||
qPostCountLTE, qUpdatedFrom, qUpdatedTo])
|
||||
qPostCountLTE, qUpdatedFrom, qUpdatedTo, qDeprecated])
|
||||
|
||||
const handleSearch = (e: FormEvent) => {
|
||||
e.preventDefault ()
|
||||
@@ -104,6 +117,8 @@ const TagListPage: FC = () => {
|
||||
setIf (qs, 'created_to', createdTo)
|
||||
setIf (qs, 'updated_from', updatedFrom)
|
||||
setIf (qs, 'updated_to', updatedTo)
|
||||
if (deprecated != null)
|
||||
qs.set ('deprecated', deprecated ? '1' : '0')
|
||||
qs.set ('page', '1')
|
||||
qs.set ('order', order)
|
||||
|
||||
@@ -201,6 +216,21 @@ const TagListPage: FC = () => {
|
||||
</>)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="状態">
|
||||
{({ invalid }) => (
|
||||
<select
|
||||
value={deprecated == null ? '' : (deprecated ? '1' : '0')}
|
||||
onChange={e => setDeprecated (
|
||||
e.target.value === ''
|
||||
? null
|
||||
: e.target.value === '1')}
|
||||
className={inputClass (invalid)}>
|
||||
<option value=""> </option>
|
||||
<option value="0">有効</option>
|
||||
<option value="1">廃止</option>
|
||||
</select>)}
|
||||
</FormField>
|
||||
|
||||
<div className="py-3">
|
||||
<button
|
||||
type="submit"
|
||||
@@ -219,6 +249,7 @@ const TagListPage: FC = () => {
|
||||
<col className="w-72"/>
|
||||
<col className="w-16"/>
|
||||
<col className="w-48"/>
|
||||
<col className="w-32"/>
|
||||
<col className="w-72"/>
|
||||
<col className="w-48"/>
|
||||
<col className="w-56"/>
|
||||
@@ -249,6 +280,7 @@ const TagListPage: FC = () => {
|
||||
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">上位タグ</th>
|
||||
<th className="p-2 text-left whitespace-nowrap">
|
||||
@@ -280,6 +312,7 @@ const TagListPage: FC = () => {
|
||||
</td>
|
||||
<td className="p-2 text-right">{row.postCount}</td>
|
||||
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
|
||||
<td className="p-2">{tagStateLabel (row.deprecatedAt)}</td>
|
||||
<td className="p-2">{row.aliases.join (' ')}</td>
|
||||
<td className="p-2">
|
||||
{row.parents.map (t => (
|
||||
|
||||
@@ -13,6 +13,7 @@ export const buildTag = (overrides: Partial<Tag> = {}): Tag => ({
|
||||
id: 1,
|
||||
name: 'テストタグ',
|
||||
category: 'general',
|
||||
deprecatedAt: null,
|
||||
aliases: [],
|
||||
parents: [],
|
||||
postCount: 12,
|
||||
|
||||
+15
-12
@@ -39,18 +39,19 @@ export type FetchTagsOrderField =
|
||||
| 'updated_at'
|
||||
|
||||
export type FetchTagsParams = {
|
||||
post: number | null
|
||||
name: string
|
||||
category: Category | null
|
||||
postCountGTE: number
|
||||
postCountLTE: number | null
|
||||
createdFrom: string
|
||||
createdTo: string
|
||||
updatedFrom: string
|
||||
updatedTo: string
|
||||
page: number
|
||||
limit: number
|
||||
order: FetchTagsOrder }
|
||||
post: number | null
|
||||
name: string
|
||||
category: Category | null
|
||||
postCountGTE: number
|
||||
postCountLTE: number | null
|
||||
createdFrom: string
|
||||
createdTo: string
|
||||
updatedFrom: string
|
||||
updatedTo: string
|
||||
deprecated: boolean | null
|
||||
page: number
|
||||
limit: number
|
||||
order: FetchTagsOrder }
|
||||
|
||||
export type FetchNicoTagsParams = {
|
||||
name: string
|
||||
@@ -196,6 +197,7 @@ export type Tag = {
|
||||
id: number
|
||||
name: string
|
||||
category: Category
|
||||
deprecatedAt: string | null
|
||||
aliases: string[]
|
||||
parents: Tag[]
|
||||
postCount: number
|
||||
@@ -213,6 +215,7 @@ export type TagVersion = {
|
||||
eventType: 'create' | 'update' | 'discard' | 'restore'
|
||||
name: { current: string; prev: string | null }
|
||||
category: { current: Category; prev: Category | null }
|
||||
deprecatedAt: { current: string | null; prev: string | null }
|
||||
aliases: { name: string; type: 'context' | 'added' | 'removed' }[]
|
||||
parentTags: { tag: Tag; type: 'context' | 'added' | 'removed' }[]
|
||||
createdAt: string
|
||||
|
||||
新しい課題から参照
ユーザをブロックする