diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index 94a7041..8d16e34 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -66,7 +66,7 @@ class TagsController < ApplicationController .offset(offset) .to_a - render json: { tags: TagRepr.base(tags), count: q.size } + render json: { tags: TagRepr.many(tags), count: q.size } end def with_depth @@ -209,6 +209,46 @@ class TagsController < ApplicationController render json: build_tag_children(tag) 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 return head :unauthorized unless current_user return head :forbidden unless current_user.gte_member? @@ -237,7 +277,7 @@ class TagsController < ApplicationController private - def build_tag_children(tag) + def build_tag_children tag material = tag.materials.first file = nil content_type = nil @@ -251,11 +291,43 @@ class TagsController < ApplicationController material: material.as_json&.merge(file:, content_type:)) end - def record_tag_version!(tag, event_type:, created_by_user:) + def record_tag_version! tag, event_type:, created_by_user: if tag.nico? NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:) else TagVersionRecorder.record!(tag:, event_type:, created_by_user:) 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 diff --git a/backend/app/representations/tag_repr.rb b/backend/app/representations/tag_repr.rb index df6925b..ecbed17 100644 --- a/backend/app/representations/tag_repr.rb +++ b/backend/app/representations/tag_repr.rb @@ -8,10 +8,9 @@ module TagRepr module_function 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 - def many tags - tags.map { |t| base(t) } - end + def many(tags) = tags.map { |t| base(t) } end diff --git a/backend/config/routes.rb b/backend/config/routes.rb index edd978f..373bb17 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -6,7 +6,7 @@ Rails.application.routes.draw do delete ':child_id', action: :destroy end - resources :tags, only: [:index, :show, :update] do + resources :tags, only: [:index, :show] do collection do get :autocomplete get :'with-depth', action: :with_depth @@ -19,6 +19,9 @@ Rails.application.routes.draw do end member do + put '', action: :update_all + patch '', action: :update + get :deerjikists end end diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7316e8b..d6e5496 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 }: { }/> }/> }/> + }/> }/> }/> }> diff --git a/frontend/src/pages/tags/TagDetailPage.tsx b/frontend/src/pages/tags/TagDetailPage.tsx new file mode 100644 index 0000000..d3ea122 --- /dev/null +++ b/frontend/src/pages/tags/TagDetailPage.tsx @@ -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 ('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 (`/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 ( + + {(loading || !(tag)) ? 'Loading...' : ( +
+ {tag.name} + +
+ {/* 名称 */} +
+ + setName (e.target.value)} + className="w-full border p-2 rounded"/> +
+ + {/* カテゴリ */} +
+ + +
+ + {/* 別名 */} +
+ + setAliases (e.target.value)} + className="w-full border p-2 rounded"/> +
+ + {/* 上位タグ */} +
+ + setParentTags (e.target.value)} + className="w-full border p-2 rounded"/> +
+ +
+ +
+
+
)} +
) +}) satisfies FC diff --git a/frontend/src/pages/tags/TagListPage.tsx b/frontend/src/pages/tags/TagListPage.tsx index e1ce2fc..6589b0d 100644 --- a/frontend/src/pages/tags/TagListPage.tsx +++ b/frontend/src/pages/tags/TagListPage.tsx @@ -260,7 +260,10 @@ export default (() => { {results.map (row => ( - + {CATEGORY_NAMES[row.category]} {row.postCount} diff --git a/frontend/src/pages/wiki/WikiDetailPage.tsx b/frontend/src/pages/wiki/WikiDetailPage.tsx index 7eab461..4ead153 100644 --- a/frontend/src/pages/wiki/WikiDetailPage.tsx +++ b/frontend/src/pages/wiki/WikiDetailPage.tsx @@ -114,13 +114,15 @@ export default () => { {...(version && { to: `/wiki/${ encodeURIComponent (title) }` })}/> {loading ?
Loading...
: } - - {(!(version) && posts.length > 0) && ( - - - - - )} + {(!(version) && posts.length > 0) && ( +
+ + + + + +
)} + ) } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 12f8838..8adb5ff 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -165,6 +165,8 @@ export type Tag = { id: number name: string category: Category + aliases: string[] + parents: Tag[] postCount: number createdAt: string updatedAt: string