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)}
>))}
-
+
))}