From 32e1054a8f142c9e780fbbbc20d81a2640e37b59 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sat, 20 Dec 2025 02:38:13 +0900 Subject: [PATCH] #184 --- frontend/src/components/TagDetailSidebar.tsx | 146 +++++++++++++------ 1 file changed, 104 insertions(+), 42 deletions(-) diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx index 7a54a85..ca4e23c 100644 --- a/frontend/src/components/TagDetailSidebar.tsx +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -12,6 +12,7 @@ import TagSearch from '@/components/TagSearch' import SectionTitle from '@/components/common/SectionTitle' import SubsectionTitle from '@/components/common/SubsectionTitle' import SidebarComponent from '@/components/layout/SidebarComponent' +import { toast } from '@/components/ui/use-toast' import { API_BASE_URL } from '@/config' import { CATEGORIES } from '@/consts' @@ -55,6 +56,31 @@ const renderTagTree = ( } +const removeEverywhere = ( + list: Tag[], + tagId: number, +): { next: Tag[]; picked?: Tag } => { + let picked: Tag | undefined + + const walk = (nodes: Tag[]): Tag[] => ( + nodes + .map (t => { + const children = t.children ? walk (t.children) : undefined + return children ? { ...t, children } : t + }) + .filter (t => { + if (t.id === tagId) + { + picked = picked ?? t + return false + } + return true + })) + + return { next: walk (list), picked } +} + + const addAsChild = ( list: Tag[], parentId: number, @@ -76,6 +102,34 @@ const addAsChild = ( } +const attachChildOptimistic = ( + prev: TagByCategory, + parentId: number, + childId: number, +): TagByCategory => { + const next: TagByCategory = { ...prev } + + let picked: Tag | undefined + for (const cat of Object.keys (next) as (keyof typeof next)[]) + { + const r = removeEverywhere (next[cat], childId) + next[cat] = r.next + picked = picked ?? r.picked + } + if (!(picked)) + return prev + + for (const cat of Object.keys (next) as (keyof typeof next)[]) + { + next[cat] = + addAsChild (next[cat], parentId, picked) + .sort ((a: Tag, b: Tag) => a.name < b.name ? -1 : 1) + } + + return next +} + + const isDescendant = ( root: Tag, targetId: number, @@ -150,10 +204,10 @@ const insertRootAt = ( } -const DropSlot = ({ cat, index }: { cat: Category, index: number }) => { +const DropSlot = ({ cat }: { cat: Category }) => { const { setNodeRef, isOver: over } = useDroppable ({ - id: `slot:${ cat }:${ index }`, - data: { kind: 'slot', cat, index } }) + id: `slot:${ cat }`, + data: { kind: 'slot', cat } }) return (
  • @@ -162,12 +216,6 @@ const DropSlot = ({ cat, index }: { cat: Category, index: number }) => { } -const removeFromRoot = ( - roots: Tag[], - childId: number, -): Tag[] => roots.filter (t => t.id !== childId) - - type Props = { post: Post | null } @@ -189,15 +237,36 @@ export default (({ post }: Props) => { const overKind = e.over?.data.current?.kind - if (!(childId) || !(overKind)) + if (childId == null || !(overKind)) return switch (overKind) { case 'tag': const parentId: number | undefined = e.over?.data.current?.tagId - if (!(parentId) || childId === parentId) + if (parentId == null || childId === parentId) + return + + try + { + if (fromParentId != null) + { + await axios.delete ( + `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, + { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) + } + + await axios.post ( + `${ API_BASE_URL }/tags/${ parentId }/children/${ childId }`, + { }, + { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) + } + catch + { + toast ({ description: '管理者権限が必要です.' }) + return + } setTags (prev => { const child = findTag (prev, childId) @@ -205,38 +274,33 @@ export default (({ post }: Props) => { if (!(child) || !(parent) || isDescendant (child, parentId)) return prev - const cat = child.category - const next: TagByCategory = { ...prev } - - if (fromParentId) - next[cat] = detachEdge (next[cat], fromParentId, childId) - else - next[cat] = removeFromRoot (next[cat], childId) - - next[cat] = addAsChild (next[cat], parentId, child) + toast ({ description: `《${ child.name }》を《${ parent.name }》の子タグに設定しました.` }) - return next + return attachChildOptimistic (prev, parentId, childId) }) - if (fromParentId) - { - await axios.delete ( - `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - } - - await axios.post ( - `${ API_BASE_URL }/tags/${ parentId }/children/${ childId }`, - { }, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - break case 'slot': const cat: Category | undefined = e.over?.data.current?.cat - const index: number | undefined = e.over?.data.current?.index - if (!(cat) || index == null) + + if (fromParentId == null + || !(cat) + || cat !== findTag (tags, childId)?.category) + return + + try + { + await axios.delete ( + `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, + { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) + } + catch + { + toast ({ description: '管理者権限が必要です.' }) + return + } setTags (prev => { const child = findTag (prev, childId) @@ -245,18 +309,16 @@ export default (({ post }: Props) => { const next: TagByCategory = { ...prev } - if (fromParentId) + if (fromParentId != null) next[cat] = detachEdge (next[cat], fromParentId, childId) as any - next[cat] = insertRootAt (next[cat], index, child) + next[cat] = insertRootAt (next[cat], 0, child) + + next[cat].sort ((a: Tag, b: Tag) => a.name < b.name ? -1 : 1) return next }) - await axios.delete ( - `${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`, - { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } }) - break } } @@ -322,7 +384,7 @@ export default (({ post }: Props) => { <> {renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined)} ))} - + ))}