This commit is contained in:
@@ -66,7 +66,7 @@ class TagsController < ApplicationController
|
|||||||
.offset(offset)
|
.offset(offset)
|
||||||
.to_a
|
.to_a
|
||||||
|
|
||||||
render json: { tags: TagRepr.base(tags), count: q.size }
|
render json: { tags: TagRepr.many(tags), count: q.size }
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_depth
|
def with_depth
|
||||||
@@ -209,6 +209,46 @@ class TagsController < ApplicationController
|
|||||||
render json: build_tag_children(tag)
|
render json: build_tag_children(tag)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_all
|
||||||
|
return head :unauthorized unless current_user
|
||||||
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
|
tag = Tag.find_by(id: params[:id])
|
||||||
|
return head :not_found unless tag
|
||||||
|
|
||||||
|
name = params[:name].to_s.strip
|
||||||
|
category = params[:category].to_s.strip
|
||||||
|
return head :unprocessable_entity if name.blank? || category.blank?
|
||||||
|
|
||||||
|
if tag.nico? != (category == 'nico')
|
||||||
|
return render json: { error: 'ニコタグのカテゴリ変更はできません.' },
|
||||||
|
status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_names = params[:aliases].to_s.split.uniq
|
||||||
|
parent_names = params[:parent_tags].to_s.split.uniq
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||||
|
|
||||||
|
old_name = tag.name
|
||||||
|
|
||||||
|
tag.update!(category:)
|
||||||
|
tag.tag_name.update!(name:)
|
||||||
|
|
||||||
|
alias_names << old_name if name != old_name
|
||||||
|
alias_names.delete(name)
|
||||||
|
|
||||||
|
update_aliases!(tag, alias_names)
|
||||||
|
update_parent_tags!(tag, parent_names)
|
||||||
|
|
||||||
|
tag.reload
|
||||||
|
record_tag_version!(tag, event_type: :update, created_by_user: current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: TagRepr.base(tag.reload)
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
@@ -237,7 +277,7 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_tag_children(tag)
|
def build_tag_children tag
|
||||||
material = tag.materials.first
|
material = tag.materials.first
|
||||||
file = nil
|
file = nil
|
||||||
content_type = nil
|
content_type = nil
|
||||||
@@ -251,11 +291,43 @@ class TagsController < ApplicationController
|
|||||||
material: material.as_json&.merge(file:, content_type:))
|
material: material.as_json&.merge(file:, content_type:))
|
||||||
end
|
end
|
||||||
|
|
||||||
def record_tag_version!(tag, event_type:, created_by_user:)
|
def record_tag_version! tag, event_type:, created_by_user:
|
||||||
if tag.nico?
|
if tag.nico?
|
||||||
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||||
else
|
else
|
||||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
TagVersionRecorder.record!(tag:, event_type:, created_by_user:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_aliases! tag, alias_names
|
||||||
|
current_aliases = tag.tag_name.aliases.to_a
|
||||||
|
|
||||||
|
current_aliases.each do |alias_tag_name|
|
||||||
|
next if alias_names.include?(alias_tag_name.name)
|
||||||
|
|
||||||
|
alias_tag_name.update!(canonical: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_names.each do |alias_name|
|
||||||
|
alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
|
||||||
|
alias_tag_name.update!(canonical: tag.tag_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_parent_tags! tag, parent_names
|
||||||
|
parent_tags = Tag.normalise_tags(parent_names, with_tagme: false,
|
||||||
|
with_no_deerjikist: false,
|
||||||
|
deny_nico: true)
|
||||||
|
|
||||||
|
TagVersioning.record_tag_snapshots!((tag.parents.to_a + parent_tags).uniq,
|
||||||
|
created_by_user: current_user)
|
||||||
|
|
||||||
|
tag.tag_implications.destroy_all
|
||||||
|
|
||||||
|
parent_tags.each do |parent_tag|
|
||||||
|
next if parent_tag == tag
|
||||||
|
|
||||||
|
TagImplication.create!(tag:, parent_tag:)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ module TagRepr
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def base tag
|
def base tag
|
||||||
tag.as_json(BASE)
|
tag.as_json(BASE).merge(aliases: tag.snapshot_aliases,
|
||||||
|
parents: tag.parents.map { _1.as_json(BASE) })
|
||||||
end
|
end
|
||||||
|
|
||||||
def many tags
|
def many(tags) = tags.map { |t| base(t) }
|
||||||
tags.map { |t| base(t) }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Rails.application.routes.draw do
|
|||||||
delete ':child_id', action: :destroy
|
delete ':child_id', action: :destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :tags, only: [:index, :show, :update] do
|
resources :tags, only: [:index, :show] do
|
||||||
collection do
|
collection do
|
||||||
get :autocomplete
|
get :autocomplete
|
||||||
get :'with-depth', action: :with_depth
|
get :'with-depth', action: :with_depth
|
||||||
@@ -19,6 +19,9 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
member do
|
member do
|
||||||
|
put '', action: :update_all
|
||||||
|
patch '', action: :update
|
||||||
|
|
||||||
get :deerjikists
|
get :deerjikists
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import PostNewPage from '@/pages/posts/PostNewPage'
|
|||||||
import PostSearchPage from '@/pages/posts/PostSearchPage'
|
import PostSearchPage from '@/pages/posts/PostSearchPage'
|
||||||
import ServiceUnavailable from '@/pages/ServiceUnavailable'
|
import ServiceUnavailable from '@/pages/ServiceUnavailable'
|
||||||
import SettingPage from '@/pages/users/SettingPage'
|
import SettingPage from '@/pages/users/SettingPage'
|
||||||
|
import TagDetailPage from '@/pages/tags/TagDetailPage'
|
||||||
import TagListPage from '@/pages/tags/TagListPage'
|
import TagListPage from '@/pages/tags/TagListPage'
|
||||||
import TheatreDetailPage from '@/pages/theatres/TheatreDetailPage'
|
import TheatreDetailPage from '@/pages/theatres/TheatreDetailPage'
|
||||||
import WikiDetailPage from '@/pages/wiki/WikiDetailPage'
|
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/:id" element={<PostDetailRoute user={user}/>}/>
|
||||||
<Route path="/posts/changes" element={<PostHistoryPage/>}/>
|
<Route path="/posts/changes" element={<PostHistoryPage/>}/>
|
||||||
<Route path="/tags" element={<TagListPage/>}/>
|
<Route path="/tags" element={<TagListPage/>}/>
|
||||||
|
<Route path="/tags/:name" element={<TagDetailPage/>}/>
|
||||||
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
|
<Route path="/tags/nico" element={<NicoTagListPage user={user}/>}/>
|
||||||
<Route path="/theatres/:id" element={<TheatreDetailPage/>}/>
|
<Route path="/theatres/:id" element={<TheatreDetailPage/>}/>
|
||||||
<Route path="/materials" element={<MaterialBasePage/>}>
|
<Route path="/materials" element={<MaterialBasePage/>}>
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
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 { fetchTagByName } from '@/lib/tags'
|
||||||
|
|
||||||
|
import type { FC, FormEvent } from 'react'
|
||||||
|
|
||||||
|
import type { Category, Tag } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
|
export default (() => {
|
||||||
|
const { name: nameRaw } = useParams ()
|
||||||
|
const tagName = String (nameRaw ?? '')
|
||||||
|
const tagKey = tagsKeys.show (tagName)
|
||||||
|
|
||||||
|
const { data: tag, isLoading: loading } = useQuery ({
|
||||||
|
queryKey: tagKey,
|
||||||
|
queryFn: () => fetchTagByName (tagName) })
|
||||||
|
|
||||||
|
const [name, setName] = useState ('')
|
||||||
|
const [category, setCategory] = useState<Category> ('general')
|
||||||
|
const [aliases, setAliases] = useState ('')
|
||||||
|
const [parentTags, setParentTags] = useState ('')
|
||||||
|
|
||||||
|
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/${ tag?.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))
|
||||||
|
return
|
||||||
|
|
||||||
|
setName (tag.name)
|
||||||
|
setCategory (tag.category as Category)
|
||||||
|
setAliases (tag.aliases?.join (' '))
|
||||||
|
setParentTags (tag.parents?.map (t => t.name).join (' '))
|
||||||
|
}, [tag])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainArea>
|
||||||
|
{(loading || !(tag)) ? 'Loading...' : (
|
||||||
|
<div className="max-w-xl">
|
||||||
|
<PageTitle>{tag.name}</PageTitle>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="my-4 space-y-2">
|
||||||
|
{/* 名称 */}
|
||||||
|
<div>
|
||||||
|
<Label>名称</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName (e.target.value)}
|
||||||
|
className="w-full border p-2 rounded"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* カテゴリ */}
|
||||||
|
<div>
|
||||||
|
<Label>カテゴリ</Label>
|
||||||
|
<select
|
||||||
|
value={category ?? ''}
|
||||||
|
onChange={e => setCategory(e.target.value as Category)}
|
||||||
|
className="w-full border p-2 rounded">
|
||||||
|
{CATEGORIES.filter (cat => cat !== 'nico').map (cat => (
|
||||||
|
<option key={cat} value={cat}>
|
||||||
|
{CATEGORY_NAMES[cat]}
|
||||||
|
</option>))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 別名 */}
|
||||||
|
<div>
|
||||||
|
<Label>別名</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={aliases}
|
||||||
|
onChange={e => setAliases (e.target.value)}
|
||||||
|
className="w-full border p-2 rounded"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 上位タグ */}
|
||||||
|
<div>
|
||||||
|
<Label>上位タグ</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={parentTags}
|
||||||
|
onChange={e => setParentTags (e.target.value)}
|
||||||
|
className="w-full border p-2 rounded"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||||
|
更新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>)}
|
||||||
|
</MainArea>)
|
||||||
|
}) satisfies FC
|
||||||
@@ -260,7 +260,10 @@ export default (() => {
|
|||||||
{results.map (row => (
|
{results.map (row => (
|
||||||
<tr key={row.id} className="even:bg-gray-100 dark:even:bg-gray-700">
|
<tr key={row.id} className="even:bg-gray-100 dark:even:bg-gray-700">
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<TagLink tag={row} withCount={false}/>
|
<TagLink
|
||||||
|
tag={row}
|
||||||
|
to={`/tags/${ encodeURIComponent (row.name) }`}
|
||||||
|
withCount={false}/>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
|
<td className="p-2">{CATEGORY_NAMES[row.category]}</td>
|
||||||
<td className="p-2 text-right">{row.postCount}</td>
|
<td className="p-2 text-right">{row.postCount}</td>
|
||||||
|
|||||||
@@ -114,13 +114,15 @@ export default () => {
|
|||||||
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
|
{...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/>
|
||||||
</h1>
|
</h1>
|
||||||
{loading ? <div>Loading...</div> : <WikiBody title={title} body={wikiPage?.body}/>}
|
{loading ? <div>Loading...</div> : <WikiBody title={title} body={wikiPage?.body}/>}
|
||||||
</article>
|
|
||||||
|
|
||||||
{(!(version) && posts.length > 0) && (
|
{(!(version) && posts.length > 0) && (
|
||||||
<TabGroup>
|
<div className="not-prose">
|
||||||
<Tab name="広場">
|
<TabGroup>
|
||||||
<PostList posts={posts}/>
|
<Tab name="広場">
|
||||||
</Tab>
|
<PostList posts={posts}/>
|
||||||
</TabGroup>)}
|
</Tab>
|
||||||
|
</TabGroup>
|
||||||
|
</div>)}
|
||||||
|
</article>
|
||||||
</MainArea>)
|
</MainArea>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ export type Tag = {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
category: Category
|
category: Category
|
||||||
|
aliases: string[]
|
||||||
|
parents: Tag[]
|
||||||
postCount: number
|
postCount: number
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
|
|||||||
Reference in New Issue
Block a user