This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
class TagChildrenController < ApplicationController
|
||||||
|
def create
|
||||||
|
return head :unauthorized unless current_user
|
||||||
|
return head :forbidden unless current_user.admin?
|
||||||
|
|
||||||
|
parent_id = params[:parent_id]
|
||||||
|
child_id = params[:child_id]
|
||||||
|
return head :bad_request if parent_id.blank? || child_id.blank?
|
||||||
|
|
||||||
|
Tag.find(parent_id).children << Tag.find(child_id) rescue nil
|
||||||
|
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
return head :unauthorized unless current_user
|
||||||
|
return head :forbidden unless current_user.admin?
|
||||||
|
|
||||||
|
parent_id = params[:parent_id]
|
||||||
|
child_id = params[:child_id]
|
||||||
|
return head :bad_request if parent_id.blank? || child_id.blank?
|
||||||
|
|
||||||
|
Tag.find(parent_id).children.delete(Tag.find(child_id)) rescue nil
|
||||||
|
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,8 @@ Rails.application.routes.draw do
|
|||||||
put 'tags/nico/:id', to: 'nico_tags#update'
|
put 'tags/nico/:id', to: 'nico_tags#update'
|
||||||
get 'tags/autocomplete', to: 'tags#autocomplete'
|
get 'tags/autocomplete', to: 'tags#autocomplete'
|
||||||
get 'tags/name/:name', to: 'tags#show_by_name'
|
get 'tags/name/:name', to: 'tags#show_by_name'
|
||||||
|
post 'tags/:parent_id/children/:child_id', to: 'tag_children#create'
|
||||||
|
delete 'tags/:parent_id/children/:child_id', to: 'tag_children#destroy'
|
||||||
get 'posts/random', to: 'posts#random'
|
get 'posts/random', to: 'posts#random'
|
||||||
get 'posts/changes', to: 'posts#changes'
|
get 'posts/changes', to: 'posts#changes'
|
||||||
post 'posts/:id/viewed', to: 'posts#viewed'
|
post 'posts/:id/viewed', to: 'posts#viewed'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { DndContext,
|
|||||||
useDroppable,
|
useDroppable,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors } from '@dnd-kit/core'
|
useSensors } from '@dnd-kit/core'
|
||||||
|
import axios from 'axios'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import TagSearch from '@/components/TagSearch'
|
|||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import SectionTitle from '@/components/common/SectionTitle'
|
||||||
import SubsectionTitle from '@/components/common/SubsectionTitle'
|
import SubsectionTitle from '@/components/common/SubsectionTitle'
|
||||||
import SidebarComponent from '@/components/layout/SidebarComponent'
|
import SidebarComponent from '@/components/layout/SidebarComponent'
|
||||||
|
import { API_BASE_URL } from '@/config'
|
||||||
import { CATEGORIES } from '@/consts'
|
import { CATEGORIES } from '@/consts'
|
||||||
|
|
||||||
import type { DragEndEvent } from '@dnd-kit/core'
|
import type { DragEndEvent } from '@dnd-kit/core'
|
||||||
@@ -44,41 +46,15 @@ const renderTagTree = (
|
|||||||
suppressClickRef={suppressClickRef}/>
|
suppressClickRef={suppressClickRef}/>
|
||||||
</motion.li>)
|
</motion.li>)
|
||||||
|
|
||||||
return [self,
|
return [
|
||||||
|
self,
|
||||||
...((tag.children
|
...((tag.children
|
||||||
?.sort ((a, b) => a.name < b.name ? -1 : 1)
|
?.sort ((a, b) => a.name < b.name ? -1 : 1)
|
||||||
.flatMap (child => (
|
.flatMap (child => renderTagTree (child, nestLevel + 1, key, suppressClickRef, tag.id)))
|
||||||
renderTagTree (child, nestLevel + 1, key, suppressClickRef, tag.id))))
|
|
||||||
?? [])]
|
?? [])]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 = (
|
const addAsChild = (
|
||||||
list: Tag[],
|
list: Tag[],
|
||||||
parentId: number,
|
parentId: number,
|
||||||
@@ -100,30 +76,6 @@ 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)
|
|
||||||
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const isDescendant = (
|
const isDescendant = (
|
||||||
root: Tag,
|
root: Tag,
|
||||||
targetId: number,
|
targetId: number,
|
||||||
@@ -210,6 +162,12 @@ 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 }
|
type Props = { post: Post | null }
|
||||||
|
|
||||||
|
|
||||||
@@ -234,8 +192,9 @@ export default (({ post }: Props) => {
|
|||||||
if (!(childId) || !(overKind))
|
if (!(childId) || !(overKind))
|
||||||
return
|
return
|
||||||
|
|
||||||
if (overKind === 'tag')
|
switch (overKind)
|
||||||
{
|
{
|
||||||
|
case 'tag':
|
||||||
const parentId: number | undefined = e.over?.data.current?.tagId
|
const parentId: number | undefined = e.over?.data.current?.tagId
|
||||||
if (!(parentId) || childId === parentId)
|
if (!(parentId) || childId === parentId)
|
||||||
return
|
return
|
||||||
@@ -246,14 +205,34 @@ export default (({ post }: Props) => {
|
|||||||
if (!(child) || !(parent) || isDescendant (child, parentId))
|
if (!(child) || !(parent) || isDescendant (child, parentId))
|
||||||
return prev
|
return prev
|
||||||
|
|
||||||
return attachChildOptimistic (prev, parentId, childId)
|
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)
|
||||||
|
|
||||||
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
if (fromParentId)
|
||||||
|
{
|
||||||
|
await axios.delete (
|
||||||
|
`${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`,
|
||||||
|
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overKind === 'slot')
|
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 cat: Category | undefined = e.over?.data.current?.cat
|
||||||
const index: number | undefined = e.over?.data.current?.index
|
const index: number | undefined = e.over?.data.current?.index
|
||||||
if (!(cat) || index == null)
|
if (!(cat) || index == null)
|
||||||
@@ -273,6 +252,12 @@ export default (({ post }: Props) => {
|
|||||||
|
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await axios.delete (
|
||||||
|
`${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`,
|
||||||
|
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
||||||
|
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user