Browse Source

#318

feature/318
みてるぞ 2 weeks ago
parent
commit
6b0d262040
8 changed files with 232 additions and 16 deletions
  1. +75
    -3
      backend/app/controllers/tags_controller.rb
  2. +3
    -4
      backend/app/representations/tag_repr.rb
  3. +4
    -1
      backend/config/routes.rb
  4. +2
    -0
      frontend/src/App.tsx
  5. +133
    -0
      frontend/src/pages/tags/TagDetailPage.tsx
  6. +4
    -1
      frontend/src/pages/tags/TagListPage.tsx
  7. +9
    -7
      frontend/src/pages/wiki/WikiDetailPage.tsx
  8. +2
    -0
      frontend/src/types.ts

+ 75
- 3
backend/app/controllers/tags_controller.rb View File

@@ -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

+ 3
- 4
backend/app/representations/tag_repr.rb View File

@@ -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
tags.map { |t| base(t) }
end
def many(tags) = tags.map { |t| base(t) }
end end

+ 4
- 1
backend/config/routes.rb View File

@@ -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


+ 2
- 0
frontend/src/App.tsx View File

@@ -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/>}>


+ 133
- 0
frontend/src/pages/tags/TagDetailPage.tsx View File

@@ -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

+ 4
- 1
frontend/src/pages/tags/TagListPage.tsx View File

@@ -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>


+ 9
- 7
frontend/src/pages/wiki/WikiDetailPage.tsx View File

@@ -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) && (
<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>) </MainArea>)
} }

+ 2
- 0
frontend/src/types.ts View File

@@ -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


Loading…
Cancel
Save