Browse Source

#184

pull/186/head
みてるぞ 3 weeks ago
parent
commit
fb9e708261
3 changed files with 100 additions and 86 deletions
  1. +27
    -0
      backend/app/controllers/tag_children_controller.rb
  2. +2
    -0
      backend/config/routes.rb
  3. +71
    -86
      frontend/src/components/TagDetailSidebar.tsx

+ 27
- 0
backend/app/controllers/tag_children_controller.rb View File

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

+ 2
- 0
backend/config/routes.rb View File

@@ -3,6 +3,8 @@ Rails.application.routes.draw do
put 'tags/nico/:id', to: 'nico_tags#update'
get 'tags/autocomplete', to: 'tags#autocomplete'
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/changes', to: 'posts#changes'
post 'posts/:id/viewed', to: 'posts#viewed'


+ 71
- 86
frontend/src/components/TagDetailSidebar.tsx View File

@@ -3,6 +3,7 @@ import { DndContext,
useDroppable,
useSensor,
useSensors } from '@dnd-kit/core'
import axios from 'axios'
import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'

@@ -11,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 { API_BASE_URL } from '@/config'
import { CATEGORIES } from '@/consts'

import type { DragEndEvent } from '@dnd-kit/core'
@@ -44,38 +46,12 @@ const renderTagTree = (
suppressClickRef={suppressClickRef}/>
</motion.li>)

return [self,
...((tag.children
?.sort ((a, b) => a.name < b.name ? -1 : 1)
.flatMap (child => (
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 }
return [
self,
...((tag.children
?.sort ((a, b) => a.name < b.name ? -1 : 1)
.flatMap (child => renderTagTree (child, nestLevel + 1, key, suppressClickRef, tag.id)))
?? [])]
}


@@ -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 = (
root: Tag,
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 }


@@ -234,46 +192,73 @@ export default (({ post }: Props) => {
if (!(childId) || !(overKind))
return

if (overKind === 'tag')
{
const parentId: number | undefined = e.over?.data.current?.tagId
if (!(parentId) || childId === parentId)
return
switch (overKind)
{
case 'tag':
const parentId: number | undefined = e.over?.data.current?.tagId
if (!(parentId) || childId === parentId)
return

setTags (prev => {
const child = findTag (prev, childId)
const parent = findTag (prev, parentId)
if (!(child) || !(parent) || isDescendant (child, parentId))
return prev

const cat = child.category
const next: TagByCategory = { ...prev }

setTags (prev => {
const child = findTag (prev, childId)
const parent = findTag (prev, parentId)
if (!(child) || !(parent) || isDescendant (child, parentId))
return prev
if (fromParentId)
next[cat] = detachEdge (next[cat], fromParentId, childId)
else
next[cat] = removeFromRoot (next[cat], childId)

return attachChildOptimistic (prev, parentId, childId)
})
next[cat] = addAsChild (next[cat], parentId, child)

return next
})

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

if (overKind === 'slot')
{
const cat: Category | undefined = e.over?.data.current?.cat
const index: number | undefined = e.over?.data.current?.index
if (!(cat) || index == null)
return
setTags (prev => {
const child = findTag (prev, childId)
if (!(child))
return prev

setTags (prev => {
const child = findTag (prev, childId)
if (!(child))
return prev
const next: TagByCategory = { ...prev }

const next: TagByCategory = { ...prev }
if (fromParentId)
next[cat] = detachEdge (next[cat], fromParentId, childId) as any

if (fromParentId)
next[cat] = detachEdge (next[cat], fromParentId, childId) as any
next[cat] = insertRootAt (next[cat], index, child)

next[cat] = insertRootAt (next[cat], index, child)
return next
})

return next
})
}
await axios.delete (
`${ API_BASE_URL }/tags/${ fromParentId }/children/${ childId }`,
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })

break
}
}

const categoryNames: Record<Category, string> = {


Loading…
Cancel
Save